Files
Jyotisha/frontend/src/lib/rectification-agentic/v9/agent-run.ts
T
Jesse_ChenandClaude Opus 5.5 0e0baaa74c fix(rectification): server fact sentences skip the evidence trim; model numbers must match server facts (BUG-1055)
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
2026-09-27 03:28:27 +08:00

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 };