/** * V10 rectification turn runner over the durable V9 Case/Evidence domains. * * Tool activity is published as it happens so the browser can render stages. * The user-visible reply is the model's terminal `text-delta`, streamed live * once that step is the spoken answer. Provider thinking stays on * `reasoning-delta` and is not a public stream event. Server narration is * only the empty-stream fallback after tools. A retried attempt emits * `attempt.reset` first so the client discards the abandoned attempt. * Durable receipts, billing, and settled history still come only from the * successful attempt. * * Phases live in their own modules (TASK-rectification-code-split-20260926): * preparation → agent-run-prepare.ts, one attempt → agent-run-attempt.ts, * the retry loop → agent-run-retry.ts, the finish → agent-run-finish.ts, * shared outcome types and receipt writers → agent-run-support.ts, the model * messages → agent-run-messages.ts. This file keeps the public types and the * order of the phases. */ import type { Agent } from "@mastra/core/agent"; import type { RectificationAgentAction } from "./step-budget"; import type { RectificationRpcClient } from "./tool-service"; import { RECTIFICATION_SKILL_NAME, RECTIFICATION_SKILL_VERSION } from "./case-status"; import type { promptCacheUsage } from "../../agent-generation-settings.ts"; import { RECTIFICATION_AGENT_ATTEMPT_TIMEOUT_MS, RECTIFICATION_AGENT_ROUTE_MAX_DURATION_S, RECTIFICATION_MIN_RETRY_ATTEMPT_MS, RECTIFICATION_RUN_BUDGET_MS, } from "../../rectification-run-budget.ts"; import type { ResolvedSkillPackageIdentity } from "../../skill-package-registry.ts"; import type { PublicStreamEvent } from "./stream-mapping"; import type { TurnIntentClassifierDiagnostic } from "./turn-intent-classifier"; import type { RectificationMessageOrigin } from "./message-origin"; import { prepareV9AgentTurn, type V9TimedTurn } from "./agent-run-prepare"; import { runV9AttemptsWithRetry } from "./agent-run-retry"; import { finishV9AgentTurn } from "./agent-run-finish"; export { buildAgentMessages, buildOpeningBrief } from "./agent-run-messages"; export type V9RunBilling = Readonly<{ reserve(): Promise<{ success: boolean; reason?: string; status: number }>; complete(input: { inputTokens: number; outputTokens: number; durationMs: number; cache?: ReturnType; }): Promise; release(): Promise; }>; export type V9AgentRunOptions = Readonly<{ userId: string; caseId: string; sessionId: string; requestId: string; action: RectificationAgentAction; message: string | null; messageOrigin?: RectificationMessageOrigin; clientActionId?: string | null; modelName: string; skillName?: string; skillVersion?: string; accounting: RectificationRpcClient; buildAgent( turnId: string, skillPackage: ResolvedSkillPackageIdentity, attemptId: string, ): Promise; billing: V9RunBilling; emit(event: PublicStreamEvent): Promise | void; signal?: AbortSignal; timeContext?: string; generationModel?: unknown; attemptTimeoutMs?: number; runBudgetMs?: number; minRetryAttemptMs?: number; expectedWrite?: "evidence" | "none" | "unknown"; collectIntent?: "classified" | "unclassified" | null; /** Classifier timing from the route preflight, for the run diagnostic only. */ classifierDiagnostic?: TurnIntentClassifierDiagnostic | null; /** * Credible range before this user message, when the route already persisted * part of the message (a typed choice answer that also carries a dated * event) before the run. Undefined: read it from the prepared dossier. */ rangeBeforeTurn?: readonly [string, string] | null; }>; export { RECTIFICATION_AGENT_ATTEMPT_TIMEOUT_MS, RECTIFICATION_AGENT_ROUTE_MAX_DURATION_S, RECTIFICATION_MIN_RETRY_ATTEMPT_MS, RECTIFICATION_RUN_BUDGET_MS, }; export type V9AgentRunResult = Readonly<{ ok: boolean; turnId: string; turnStatus: "completed" | "failed" | "retryable"; skillLoaded: boolean; answerText: string; phases: readonly string[]; toolsUsed: readonly string[]; errorCode: string | null; previousFocusId: string | null; /** * True when this run's own post-turn interview call found the next focus * already active and linked it to this turn (BUG-1047 D5). The route's * common exit gate then skips its second, identical re-read. */ interviewSettled?: boolean; }>; export async function runV9AgentTurn(options: V9AgentRunOptions): Promise { // Preparation either answers the turn outright (year re-ask, already // delivered, idempotent replay) or hands back the pending turn to run. const prepared = await prepareV9AgentTurn(options); if ("ok" in prepared) return prepared; const startedAt = Date.now(); const runBudgetMs = options.runBudgetMs ?? RECTIFICATION_RUN_BUDGET_MS; const minRetryAttemptMs = options.minRetryAttemptMs ?? RECTIFICATION_MIN_RETRY_ATTEMPT_MS; const attemptCapMs = options.attemptTimeoutMs ?? RECTIFICATION_AGENT_ATTEMPT_TIMEOUT_MS; const runDeadlineAt = startedAt + runBudgetMs; const turn: V9TimedTurn = { ...prepared, startedAt, runBudgetMs, minRetryAttemptMs, attemptCapMs, runDeadlineAt, }; const { emit } = turn; await emit({ type: "run.started" }); const outcome = await runV9AttemptsWithRetry(turn); return finishV9AgentTurn(turn, outcome); } export { RECTIFICATION_SKILL_NAME, RECTIFICATION_SKILL_VERSION };