import { randomUUID } from 'node:crypto' import { registerCommand } from '@open-mercato/shared/lib/commands' import type { CommandHandler } from '@open-mercato/shared/lib/commands' import type { EntityManager } from '@mikro-orm/postgresql' import { z } from 'zod' import { AgentRun, type AgentRunStatus } from '../data/entities' import { agentTypeSchema } from '../data/validators' import { emitAgentOrchestratorEvent } from '../events' import { getRerunOfRunId } from '../lib/runtime/rerunContext' import { invalidateAgentRunCache } from '../lib/crudCache' const createAgentRunSchema = z.object({ tenantId: z.string().uuid(), organizationId: z.string().uuid(), agentId: z.string().min(1), input: z.unknown(), /** Parent run id for a nested sub-agent run (Phase 4); null/absent for top-level runs. */ parentRunId: z.string().uuid().nullable().optional(), /** * Runtime that produced this run; half of the trace-ingestion idempotency key * `(runtime, externalRunId)`. Stamped at creation so a later trace POST for the * same run upserts THIS row instead of creating a duplicate. Optional + nullable * so existing callers keep compiling. */ runtime: z.string().min(1).nullable().optional(), /** Runtime-native run id; the other half of the ingestion idempotency key. */ externalRunId: z.string().min(1).nullable().optional(), /** * Native runtime (spec decision H2): when true and no `externalRunId` is * supplied, the command pre-generates the run's uuid and stamps * `externalRunId = id` in the SAME insert, so a later trace ingest for * `(runtime, externalRunId=runId)` upserts THIS row instead of creating a * shadow duplicate. Additive; ignored when an explicit `externalRunId` is set. */ stampExternalRunIdFromId: z.boolean().optional(), /** `eval` marks a replay so it never skews the agent's production metrics. */ source: z.enum(['runtime', 'eval']).optional(), /** Declared model id; stamped so the cockpit can show/filter runs by model. Null when the agent uses the tenant default. */ model: z.string().min(1).max(100).nullable().optional(), /** Workflow process instance id this run belongs to (INVOKE_AGENT step); links the run to the process in traces. */ workflowInstanceId: z.string().uuid().nullable().optional(), /** Workflow step id this run belongs to. */ stepId: z.string().min(1).nullable().optional(), /** The step's attempt id — with instance + step it IS the invocation's identity. */ invocationId: z.string().min(1).max(100).nullable().optional(), /** * The agent's DECLARED type, stamped onto the run record. Nullable + optional: an * agent that declares none, and every caller written before the declaration existed, * leave the column null. */ agentType: agentTypeSchema.nullable().optional(), }) export type CreateAgentRunInput = z.infer /** * Data-honesty additive stamps (spec §3.2): confidence + token/cost fields are * optional — an ABSENT field leaves the column untouched, so existing callers * are byte-for-byte unaffected. Cost is an estimate computed by the caller * (see `lib/runtime/modelPricing.ts`), stored once, never recomputed at read. */ const runUsageStampSchema = z.object({ inputTokens: z.number().int().nonnegative().nullable().optional(), outputTokens: z.number().int().nonnegative().nullable().optional(), costMinor: z.number().int().nonnegative().nullable().optional(), currency: z.string().length(3).nullable().optional(), }) const completeAgentRunSchema = z .object({ runId: z.string().uuid(), status: z.enum(['ok', 'error']), output: z.unknown().optional(), resultKind: z.enum(['research', 'proposal', 'artifact']).nullable().optional(), confidence: z.number().min(0).max(1).nullable().optional(), }) .merge(runUsageStampSchema) export type CompleteAgentRunInput = z.infer const failAgentRunSchema = z .object({ runId: z.string().uuid(), errorMessage: z.string(), }) .merge(runUsageStampSchema) export type FailAgentRunInput = z.infer /** Apply the optional usage/cost stamps; absent (undefined) fields leave columns untouched. */ function applyUsageStamp(run: AgentRun, input: z.infer): void { if (input.inputTokens !== undefined) run.inputTokens = input.inputTokens if (input.outputTokens !== undefined) run.outputTokens = input.outputTokens if (input.costMinor !== undefined) run.costMinor = input.costMinor if (input.currency !== undefined) run.currency = input.currency } export const createAgentRunCommand: CommandHandler = { id: 'agent_orchestrator.runs.create', async execute(rawInput, ctx) { const input = createAgentRunSchema.parse(rawInput) const em = (ctx.container.resolve('em') as EntityManager).fork() const selfStampedId = input.stampExternalRunIdFromId && !input.externalRunId ? randomUUID() : null const run = em.create(AgentRun, { source: input.source ?? 'runtime', ...(selfStampedId ? { id: selfStampedId } : {}), tenantId: input.tenantId, organizationId: input.organizationId, agentId: input.agentId, status: 'running' as AgentRunStatus, input: input.input, parentRunId: input.parentRunId ?? null, // Re-run lineage: only the top-level run of a trace-inspector re-run is // stamped; nested delegations carry parentRunId and skip it. rerunOfRunId: input.parentRunId ? null : getRerunOfRunId() ?? null, runtime: input.runtime ?? null, externalRunId: selfStampedId ?? input.externalRunId ?? null, model: input.model ?? null, workflowInstanceId: input.workflowInstanceId ?? null, stepId: input.stepId ?? null, invocationId: input.invocationId ?? null, agentType: input.agentType ?? null, }) em.persist(run) await em.flush() await invalidateAgentRunCache( ctx.container, { id: run.id, tenantId: run.tenantId, organizationId: run.organizationId }, 'agent_orchestrator.runs.create', ) await emitAgentOrchestratorEvent('agent_orchestrator.run.created', { id: run.id, agentId: run.agentId, tenantId: run.tenantId, organizationId: run.organizationId, }, { persistent: true }) return { runId: run.id } }, } export const completeAgentRunCommand: CommandHandler = { id: 'agent_orchestrator.runs.complete', async execute(rawInput, ctx) { const input = completeAgentRunSchema.parse(rawInput) const em = (ctx.container.resolve('em') as EntityManager).fork() const run = await em.findOne(AgentRun, { id: input.runId }) if (!run) throw new Error(`[internal] agent run not found: ${input.runId}`) run.status = input.status run.output = input.output ?? null run.resultKind = input.resultKind ?? null if (input.confidence !== undefined) run.confidence = input.confidence applyUsageStamp(run, input) // Forensic fact: stamped once at the terminal transition, never overwritten // (same flush as the status change — atomic per row). if (!run.completedAt) run.completedAt = new Date() run.updatedAt = new Date() await em.flush() await invalidateAgentRunCache( ctx.container, { id: run.id, tenantId: run.tenantId, organizationId: run.organizationId }, 'agent_orchestrator.runs.complete', ) await emitAgentOrchestratorEvent('agent_orchestrator.run.completed', { id: run.id, agentId: run.agentId, status: run.status, resultKind: run.resultKind, tenantId: run.tenantId, organizationId: run.organizationId, }, { persistent: true }) return { runId: run.id } }, } export const failAgentRunCommand: CommandHandler = { id: 'agent_orchestrator.runs.fail', async execute(rawInput, ctx) { const input = failAgentRunSchema.parse(rawInput) const em = (ctx.container.resolve('em') as EntityManager).fork() const run = await em.findOne(AgentRun, { id: input.runId }) if (!run) throw new Error(`[internal] agent run not found: ${input.runId}`) run.status = 'error' run.errorMessage = input.errorMessage // Failed runs still consumed tokens — stamp usage/cost when supplied. applyUsageStamp(run, input) if (!run.completedAt) run.completedAt = new Date() run.updatedAt = new Date() await em.flush() await invalidateAgentRunCache( ctx.container, { id: run.id, tenantId: run.tenantId, organizationId: run.organizationId }, 'agent_orchestrator.runs.fail', ) await emitAgentOrchestratorEvent('agent_orchestrator.run.completed', { id: run.id, agentId: run.agentId, status: run.status, errorMessage: run.errorMessage, tenantId: run.tenantId, organizationId: run.organizationId, }, { persistent: true }) return { runId: run.id } }, } registerCommand(createAgentRunCommand) registerCommand(completeAgentRunCommand) registerCommand(failAgentRunCommand)