T1 of TASK-rectification-grounding-20260927 (recurrence of BUG-588). - The attempt no longer streams range-changed / rescore-skipped / compare-failed sentences; the finish whitelists and trims the model body, then joins the server facts, and emits one final replace equal to the persisted text. - P3 whitelist (spoken-grounding.ts): a model sentence with a clock, clock range or percentage that is not this turn's server fact is dropped whole; the batch recap stands in when nothing is left. - record-evidence-batch returns range_after_rescore (post-rescore credible_range, representative minute, fit percent, delivers_range_this_turn); the receipt fingerprint stays over the old shape. - System prompt: range is said by the server; the delivery three sentences only when the batch says this turn delivers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
142 lines
5.4 KiB
TypeScript
142 lines
5.4 KiB
TypeScript
/**
|
|
* 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<typeof promptCacheUsage>;
|
|
}): Promise<boolean>;
|
|
release(): Promise<boolean>;
|
|
}>;
|
|
|
|
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<Agent>;
|
|
billing: V9RunBilling;
|
|
emit(event: PublicStreamEvent): Promise<void> | 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<V9AgentRunResult> {
|
|
// 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 };
|