Files
Jyotisha/frontend/tests/rectification-v9-stream.test.ts
Jesse_Chen 97b02aef5d
Independent Staging Quality Gate / validate (push) Successful in 22m10s
Independent Staging Quality Gate / publish (push) Has started running
fix(web): hold reverse-inference cards until acceptance event quality
Conflict probes were jumping after one dated event, so the interview asked
another domain before method collection. Spoken replies now follow the
stamped choice prompt instead of a topic denylist.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 23:53:42 +08:00

1540 lines
64 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
mapStreamChunkToActivity,
mapStreamChunkToPhase,
mapStreamChunkToThinking,
toPublicThinkingDelta,
safePublicEvent,
streamToolNames,
} from "../src/lib/rectification-agentic/v9/stream-mapping.ts";
import { PUBLIC_RECTIFICATION_TOOLS } from "../src/lib/rectification-agentic/v9/public-receipt.ts";
import { runV9AgentTurn, type V9AgentRunOptions } from "../src/lib/rectification-agentic/v9/agent-run.ts";
import {
CASE_ID,
SESSION_ID,
TURN_ID,
USER_ID,
dossierFixture,
fakeAccounting,
receiptHandlers,
} from "./rectification-v9-test-support.ts";
import { RECTIFICATION_SKILL_NAME } from "../src/lib/rectification-agentic/v9/case-status.ts";
import { messageContentHash } from "../src/lib/rectification-agentic/v9/message-origin.ts";
import { safeToolErrorCode } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import {
bindSpokenToOpenQuestion,
openQuestionPromptFromToolResult,
} from "../src/lib/rectification-agentic/v9/turn-narration.ts";
import {
createRectificationActivityReceiptState,
receiptFromRectificationActivityState,
reduceRectificationActivityReceipt,
} from "../src/lib/rectification-activity-receipt.ts";
type StreamChunk = {
type: string;
payload?: {
toolName?: unknown;
text?: unknown;
args?: unknown;
error?: unknown;
result?: unknown;
output?: unknown;
};
object?: unknown;
};
function chunk(type: string, payload?: Record<string, unknown>): StreamChunk {
return { type, ...(payload ? { payload } : {}) };
}
test("fullStream chunks map to the allowlisted NDJSON phases only", () => {
assert.equal(mapStreamChunkToPhase(chunk("start") as never), null);
assert.equal(
mapStreamChunkToPhase(chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }) as never),
null,
);
assert.deepEqual(
mapStreamChunkToPhase(chunk("tool-result", { toolName: "skill" }) as never),
{ type: "skill.bound" },
);
assert.deepEqual(
mapStreamChunkToPhase(chunk("tool-call", { toolName: "rectification-compare-candidates" }) as never),
{ type: "candidates.comparing", tool: "rectification-compare-candidates" },
);
assert.deepEqual(
mapStreamChunkToPhase(chunk("tool-result", {
toolName: "rectification-compare-candidates",
result: {
executed_methods: [
"d1-rashi",
"vimshottari-dasha",
"internal-secret-technique",
"d1-rashi",
],
birth_context: { birth_date: "1997-08-08" },
event_contribution_matrix: { secret: true },
rule_ids: ["internal-rule"],
},
}) as never),
{
type: "candidates.updated",
tool: "rectification-compare-candidates",
methods: ["d1-rashi", "vimshottari-dasha"],
},
);
assert.equal(
mapStreamChunkToPhase(chunk("tool-call", { toolName: "rectification-read-case" }) as never),
null,
);
assert.deepEqual(
mapStreamChunkToPhase(chunk("tool-result", {
toolName: "rectification-read-case",
result: { birth_context: { birth_date: "1997-08-08", latitude: 36.4 } },
}) as never),
{ type: "case.loaded", tool: "rectification-read-case" },
);
assert.equal(
mapStreamChunkToPhase(chunk("text-delta", { text: "你好" }) as never),
null,
);
assert.equal(mapStreamChunkToPhase(chunk("finish") as never), null);
assert.equal(mapStreamChunkToPhase(chunk("error", { error: new Error("boom") }) as never), null);
assert.equal(mapStreamChunkToPhase(chunk("abort") as never), null);
});
test("every public rectification tool maps its real lifecycle to public activity", () => {
for (const tool of PUBLIC_RECTIFICATION_TOOLS) {
assert.deepEqual(
mapStreamChunkToActivity(chunk("tool-call", {
toolName: tool,
args: { caseId: CASE_ID, birth_date: "1997-08-08", scores: [99] },
}) as never),
{ type: "tool.activity", tool, status: "started" },
);
assert.deepEqual(
mapStreamChunkToActivity(chunk("tool-result", {
toolName: tool,
result: {
executed_methods: ["d1-rashi", "private-method", "d1-rashi"],
birth_context: { birth_date: "1997-08-08" },
scores: [99],
},
}) as never),
{ type: "tool.activity", tool, status: "completed", methods: ["d1-rashi"] },
);
assert.deepEqual(
mapStreamChunkToActivity(chunk("tool-error", {
toolName: tool,
args: { caseId: CASE_ID, scores: [99] },
error: new Error("private provider error"),
}) as never),
{ type: "tool.activity", tool, status: "failed" },
);
}
assert.equal(mapStreamChunkToActivity(chunk("tool-call", { toolName: "skill" }) as never), null);
assert.equal(mapStreamChunkToActivity(chunk("tool-error", { toolName: "private-tool" }) as never), null);
assert.equal(mapStreamChunkToActivity(chunk("text-delta", { text: "x" }) as never), null);
});
test("evidence-batch rescore methods surface on the public activity event", () => {
assert.deepEqual(
mapStreamChunkToActivity(chunk("tool-result", {
toolName: "rectification-record-evidence-batch",
result: {
accepted_count: 1,
rescore: {
status: "completed",
executed_methods: ["d1-rashi", "d10-dashamsa", "private-method"],
},
},
}) as never),
{
type: "tool.activity",
tool: "rectification-record-evidence-batch",
status: "completed",
methods: ["d1-rashi", "d10-dashamsa"],
},
);
});
test("reasoning, raw payloads, provider metadata and step internals never map to the answer channel", () => {
assert.equal(mapStreamChunkToPhase(chunk("reasoning-start", { id: "r1" }) as never), null);
assert.equal(mapStreamChunkToPhase(chunk("reasoning-delta", { text: "内部推理" }) as never), null);
assert.equal(mapStreamChunkToPhase(chunk("reasoning-end") as never), null);
assert.equal(mapStreamChunkToPhase(chunk("raw", { payload: { secret: true } }) as never), null);
assert.equal(mapStreamChunkToPhase(chunk("step-start", { messageId: "m1" }) as never), null);
assert.equal(mapStreamChunkToPhase(chunk("step-finish", { output: { text: "x" } }) as never), null);
assert.equal(mapStreamChunkToPhase(chunk("response-metadata", { signature: "s" }) as never), null);
assert.equal(mapStreamChunkToPhase(chunk("source", { title: "t" }) as never), null);
assert.equal(mapStreamChunkToPhase(chunk("file", { mimeType: "text/plain" }) as never), null);
});
test("Chinese thinking stays internal and is never a public stream event", () => {
assert.deepEqual(
mapStreamChunkToThinking(chunk("reasoning-delta", { text: "先核对升学年份。" }) as never),
{ type: "thinking.delta", text: "先核对升学年份。" },
);
assert.equal(
mapStreamChunkToThinking(chunk("reasoning-delta", {
text: "The proposedKind value was rejected",
}) as never),
null,
);
assert.equal(
toPublicThinkingDelta("The proposedKind value was rejected because education is invalid"),
null,
);
assert.equal(safePublicEvent({ type: "thinking.delta", text: "先核对升学年份。" }), null);
assert.equal(safePublicEvent({
type: "thinking.delta",
text: "先核对升学年份。",
turnId: TURN_ID,
args: { caseId: CASE_ID },
}), null);
});
test("streamToolNames exposes only allowlisted rectification tools", () => {
assert.deepEqual(streamToolNames(chunk("tool-call", { toolName: "rectification-read-case" }) as never), ["rectification-read-case"]);
assert.deepEqual(streamToolNames(chunk("tool-call", { toolName: "skill" }) as never), []);
assert.deepEqual(streamToolNames(chunk("tool-call", { toolName: "rectification-gate" }) as never), []);
assert.deepEqual(streamToolNames(chunk("text-delta", { text: "x" }) as never), []);
});
test("safePublicEvent drops anything outside the allowlist", () => {
assert.deepEqual(safePublicEvent({ type: "answer.delta", text: "你好" }), { type: "answer.delta", text: "你好" });
assert.equal(safePublicEvent({ type: "thinking.delta", text: "先核对升学" }), null);
assert.deepEqual(
safePublicEvent({ type: "activity.changed", activity: "reading_case" }),
{ type: "activity.changed", activity: "reading_case" },
);
assert.deepEqual(safePublicEvent({ type: "attempt.reset" }), { type: "attempt.reset" });
assert.deepEqual(safePublicEvent({ type: "skill.loaded" }), { type: "skill.loaded" });
assert.deepEqual(
safePublicEvent({ type: "run.completed", turnId: TURN_ID }),
{ type: "run.completed", turnId: TURN_ID },
);
assert.deepEqual(
safePublicEvent({ type: "answer.delta", text: "你好", turnId: TURN_ID }),
{ type: "answer.delta", text: "你好" },
);
assert.deepEqual(
safePublicEvent({ type: "run.completed", turnId: "not-a-uuid" }),
{ type: "run.completed" },
);
assert.deepEqual(
safePublicEvent({
type: "case.loaded",
tool: "rectification-read-case",
text: "1997-08-08 河北省邯郸市",
methods: ["d1-rashi"],
birth_context: { latitude: 36.4 },
}),
{ type: "case.loaded", tool: "rectification-read-case" },
);
assert.deepEqual(
safePublicEvent({
type: "tool.activity",
tool: "rectification-compare-candidates",
status: "completed",
methods: ["d10-dashamsa", "private-method", "d10-dashamsa"],
args: { caseId: CASE_ID },
error: "private provider error",
birth_context: { birth_date: "1997-08-08" },
scores: [99],
}),
{
type: "tool.activity",
tool: "rectification-compare-candidates",
status: "completed",
methods: ["d10-dashamsa"],
},
);
assert.deepEqual(
safePublicEvent({
type: "tool.activity",
tool: "rectification-read-case",
status: "started",
methods: ["d1-rashi"],
result: { private: true },
}),
{ type: "tool.activity", tool: "rectification-read-case", status: "started" },
);
assert.deepEqual(
safePublicEvent({
type: "tool.activity",
tool: "rectification-read-case",
status: "failed",
error: "private provider error",
}),
{ type: "tool.activity", tool: "rectification-read-case", status: "failed" },
);
assert.equal(safePublicEvent({ type: "tool.activity", tool: "private-tool", status: "started" }), null);
assert.equal(safePublicEvent({ type: "tool.activity", tool: "rectification-read-case", status: "pending" }), null);
assert.deepEqual(
safePublicEvent({
type: "candidates.updated",
tool: "rectification-compare-candidates",
methods: ["d10-dashamsa", "private-method", "d10-dashamsa"],
rule_ids: ["private-rule"],
}),
{
type: "candidates.updated",
tool: "rectification-compare-candidates",
methods: ["d10-dashamsa"],
},
);
assert.equal(safePublicEvent({ type: "provider.reasoning", text: "内部" }), null);
assert.equal(safePublicEvent({ type: "tool.payload", text: "秘密" }), null);
assert.equal(safePublicEvent({ type: "raw" }), null);
assert.equal(safePublicEvent(null), null);
assert.deepEqual(
safePublicEvent({ type: "error", code: "skill_identity_unverifiable", message: "请先采用当前 Skill" }),
{ type: "error", code: "skill_identity_unverifiable", message: "请先采用当前 Skill" },
);
assert.equal(safePublicEvent({ type: "error", code: "private_error", message: "secret" }), null);
});
type FakeStreamResult = {
fullStream: AsyncIterable<{ type: string; payload?: Record<string, unknown> }>;
totalUsage?: Promise<{ inputTokens?: number; outputTokens?: number }>;
};
function fakeAgentStream(chunks: Array<{ type: string; payload?: Record<string, unknown> }>) {
return {
stream: async () => ({
fullStream: (async function* () {
for (const item of chunks) yield item;
})(),
totalUsage: Promise.resolve({ inputTokens: 10, outputTokens: 20 }),
}) as FakeStreamResult,
getSkill: async () => ({ name: RECTIFICATION_SKILL_NAME, instructions: "skill" }),
};
}
function runOptions(overrides: Partial<V9AgentRunOptions> = {}) {
const emitted: Array<{ type: string; text?: string; turnId?: string }> = [];
const billing = { reserved: 0, completed: 0, released: 0 };
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }),
});
const optionsValue: V9AgentRunOptions = {
userId: USER_ID,
caseId: CASE_ID,
sessionId: SESSION_ID,
requestId: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
action: "evidence",
message: "2016年9月离开家去北京工作",
modelName: "gpt-4o-mini",
accounting: accounting.client,
billing: {
reserve: async () => { billing.reserved += 1; return { success: true, status: 200 }; },
complete: async () => { billing.completed += 1; return true; },
release: async () => { billing.released += 1; return true; },
},
emit: (event) => { emitted.push(event); },
buildAgent: async () => fakeAgentStream([]) as never,
...overrides,
};
return { options: optionsValue, emitted, billing };
}
test("request-aware append receives the caller request id", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }),
});
const { options } = runOptions({
accounting: accounting.client,
buildAgent: async () => fakeAgentStream([
chunk("tool-result", { toolName: "skill" }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("text-delta", { text: "已记录" }),
chunk("finish"),
]) as never,
});
await runV9AgentTurn(options);
const append = accounting.calls.find((call) => call.fn === "append_agentic_rectification_turn");
assert.equal(append?.args.p_request_id, options.requestId);
});
test("completed request replay returns persisted truth without rebuilding or settling", async () => {
let buildCount = 0;
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({ status: "confirmed" }),
append_agentic_rectification_turn: () => ({
turn_id: TURN_ID,
status: "completed",
assistant_message: "这是已持久化的回答",
successful_attempt_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
should_execute: false,
already_in_progress: false,
idempotent: true,
}),
});
const { options, emitted, billing } = runOptions({
accounting: accounting.client,
buildAgent: async () => {
buildCount += 1;
return fakeAgentStream([]) as never;
},
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
assert.equal(result.turnId, TURN_ID);
assert.equal(result.answerText, "这是已持久化的回答");
assert.equal(buildCount, 0);
assert.deepEqual(billing, { reserved: 1, completed: 0, released: 0 });
assert.deepEqual(emitted, [
{ type: "run.started" },
{ type: "answer.delta", text: "这是已持久化的回答" },
{ type: "run.completed", turnId: TURN_ID },
]);
assert.equal(
accounting.calls.some((call) => call.fn === "create_agentic_rectification_run_attempt"),
false,
);
});
test("pending request replay never rebuilds or releases the original reservation", async () => {
let buildCount = 0;
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({
turn_id: TURN_ID,
status: "pending",
assistant_message: null,
successful_attempt_id: null,
should_execute: false,
already_in_progress: true,
idempotent: true,
}),
});
const { options, billing } = runOptions({
accounting: accounting.client,
buildAgent: async () => {
buildCount += 1;
return fakeAgentStream([]) as never;
},
});
await assert.rejects(
runV9AgentTurn(options),
(error: unknown) => error instanceof Error
&& error.message.includes("agentic_rectification_turn_in_progress"),
);
assert.equal(buildCount, 0);
assert.deepEqual(billing, { reserved: 1, completed: 0, released: 0 });
});
for (const finalizedStatus of ["failed", "retryable"] as const) {
test(`${finalizedStatus} request replay does not rebuild and releases only the current claim`, async () => {
let buildCount = 0;
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({
turn_id: TURN_ID,
status: finalizedStatus,
assistant_message: null,
successful_attempt_id: null,
should_execute: false,
already_in_progress: false,
idempotent: true,
}),
});
const { options, billing } = runOptions({
accounting: accounting.client,
buildAgent: async () => {
buildCount += 1;
return fakeAgentStream([]) as never;
},
});
await assert.rejects(
runV9AgentTurn(options),
(error: unknown) => error instanceof Error
&& error.message.includes("agentic_rectification_turn_already_finalized"),
);
assert.equal(buildCount, 0);
assert.deepEqual(billing, { reserved: 1, completed: 0, released: 1 });
});
}
test("answer deltas stream in order and reasoning is never forwarded", async () => {
const { options, emitted } = runOptions({
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("reasoning-start", { id: "r1" }),
chunk("reasoning-delta", { text: "我应该先……" }),
chunk("reasoning-end"),
chunk("text-delta", { text: "好的," }),
chunk("text-delta", { text: "先确认一下:" }),
chunk("raw", { payload: { tool_args: { secret: true } } }),
chunk("finish"),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
const deltas = emitted.filter((event) => event.type === "answer.delta");
assert.deepEqual(deltas, [
{ type: "answer.delta", text: "好的,先确认一下:" },
]);
assert.deepEqual(
emitted.filter((event) => event.type === "thinking.delta"),
[],
);
assert.equal(emitted.some((event) => event.type === "activity.changed"), true);
assert.deepEqual(
emitted.find((event) => event.type === "run.completed"),
{ type: "run.completed", turnId: TURN_ID },
);
assert.equal(emitted.some((event) => String(event.type).includes("reasoning")), false);
assert.equal(emitted.some((event) => String(event.type).includes("raw")), false);
});
test("English tool-retry narration never becomes the spoken answer", async () => {
const { options, emitted } = runOptions({
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("tool-call", {
toolName: "rectification-record-evidence-batch",
args: { caseId: CASE_ID, proposedKind: "education" },
}),
chunk("tool-error", {
toolName: "rectification-record-evidence-batch",
error: new Error("invalid_event_kind"),
}),
chunk("text-delta", {
text: "The proposedKind value was rejected. Retrying with education_start.",
}),
chunk("reasoning-delta", { text: "先改用升学开始。" }),
chunk("tool-call", {
toolName: "rectification-record-evidence-batch",
args: { caseId: CASE_ID, proposedKind: "education_start" },
}),
chunk("tool-result", { toolName: "rectification-record-evidence-batch" }),
chunk("text-delta", { text: "记下了,2016年9月上大学。" }),
chunk("finish"),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
assert.deepEqual(
emitted.filter((event) => event.type === "answer.delta"),
[{ type: "answer.delta", text: "记下了,2016年9月上大学。" }],
);
assert.deepEqual(
emitted.filter((event) => event.type === "thinking.delta"),
[],
);
const publicText = JSON.stringify(emitted);
assert.doesNotMatch(publicText, /The proposedKind value was rejected/);
assert.doesNotMatch(publicText, /invalid_event_kind/);
});
test("Chinese process self-talk after tools is thinking, not the spoken answer", async () => {
const { options, emitted } = runOptions({
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("tool-call", {
toolName: "rectification-record-evidence-batch",
args: { caseId: CASE_ID, proposedKind: "education_start" },
}),
chunk("tool-result", { toolName: "rectification-record-evidence-batch" }),
chunk("reasoning-delta", {
text: "用户提到先给了一个很晚的年份,后又改口说六岁入学。这里有个明显的内部矛盾。\n\n",
}),
chunk("reasoning-delta", {
text: "但按 skill 规则,日期精度真实保留,不得猜补。datePrecision 用 year。\n\n",
}),
chunk("text-delta", {
text: "记下了,大约六岁入学小学。接下来你大概哪一年上的初中?",
}),
chunk("finish"),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
assert.deepEqual(
emitted.filter((event) => event.type === "answer.delta").map((event) => event.text).join(""),
"记下了,大约六岁入学小学。接下来你大概哪一年上的初中?",
);
const thinking = emitted
.filter((event) => event.type === "thinking.delta")
.map((event) => event.text)
.join("");
assert.equal(thinking, "");
assert.equal(emitted.some((event) => String(event.type).includes("reasoning")), false);
assert.equal(result.answerText, "记下了,大约六岁入学小学。接下来你大概哪一年上的初中?");
});
test("process-only self-talk after tools is replaced by server narration, not retried", async () => {
let buildCount = 0;
const processTalk = "用户在上一轮里提供了两件带日期的经历。我需要用批量工具写入这些证据。用户";
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }),
});
const { options, emitted, billing } = runOptions({
accounting: accounting.client,
buildAgent: async () => {
buildCount += 1;
return attemptStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("tool-call", {
toolName: "rectification-record-evidence-batch",
args: { caseId: CASE_ID, proposedKind: "education_start" },
}),
chunk("tool-result", { toolName: "rectification-record-evidence-batch" }),
chunk("reasoning-delta", { text: processTalk }),
chunk("finish"),
], { inputTokens: 11, outputTokens: 12 }) as never;
},
});
const result = await runV9AgentTurn(options);
assert.equal(buildCount, 1);
assert.equal(result.ok, true);
assert.match(result.answerText, /已经记下|请继续说下一件/);
assert.equal(emitted.some((event) => event.type === "attempt.reset"), false);
assert.doesNotMatch(JSON.stringify(emitted), /我需要用批量工具/);
const finalizedTurn = accounting.calls.find((call) => call.fn === "finalize_agentic_rectification_turn");
assert.equal(finalizedTurn?.args.p_status, "completed");
assert.deepEqual(billing, { reserved: 1, completed: 1, released: 0 });
});
test("a length-limited spoken answer is not billed or persisted as a completed turn", async () => {
const pinched = "**先看候选结构(本会话以代表性时间收口";
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "failed", idempotent: false }),
});
const { options, emitted, billing } = runOptions({
accounting: accounting.client,
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("text-delta", { text: pinched }),
chunk("finish", { stepResult: { reason: "length" } }),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, false);
assert.equal(result.errorCode, "answer_truncated");
assert.deepEqual(billing, { reserved: 1, completed: 0, released: 1 });
assert.equal(emitted.filter((event) => event.type === "run.completed").length, 0);
assert.equal(emitted.some((event) => event.type === "run.failed"), true);
assert.deepEqual(
emitted.filter((event) => event.type === "answer.delta"),
[{ type: "answer.delta", text: pinched }],
);
const turnFinalize = accounting.calls.find((call) => call.fn === "finalize_agentic_rectification_turn");
assert.equal(turnFinalize?.args.p_assistant_message, null);
assert.equal(turnFinalize?.args.p_successful_attempt_id, null);
});
test("answer deltas and tool activity are published before billing settles", async () => {
let billingStarted = false;
const seenBeforeBilling: string[] = [];
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }),
});
const { options, emitted, billing } = runOptions({
accounting: accounting.client,
billing: {
reserve: async () => { billing.reserved += 1; return { success: true, status: 200 }; },
complete: async () => {
billingStarted = true;
billing.completed += 1;
return true;
},
release: async () => { billing.released += 1; return true; },
},
emit: (event) => {
if (!billingStarted && (event.type === "answer.delta" || event.type === "tool.activity" || event.type === "answer.composed")) {
seenBeforeBilling.push(event.type);
}
emitted.push(event);
},
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("text-delta", { text: "先记下这件事。" }),
chunk("finish"),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
assert.ok(seenBeforeBilling.includes("tool.activity"));
assert.ok(seenBeforeBilling.includes("answer.delta"));
assert.ok(seenBeforeBilling.includes("answer.composed"));
const deltaIndex = emitted.findIndex((event) => event.type === "answer.delta");
const billingIndex = emitted.findIndex((event) => event.type === "billing.settled");
assert.ok(deltaIndex >= 0 && billingIndex > deltaIndex);
});
test("half-failure never becomes settled history and releases usage", async () => {
const { options, emitted, billing } = runOptions({
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("text-delta", { text: "正在计算," }),
chunk("error", { error: new Error("provider failure") }),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, false);
assert.equal(result.turnStatus, "retryable");
assert.equal(billing.released, 1);
assert.equal(billing.completed, 0);
assert.equal(emitted.some((event) => event.type === "run.failed"), true);
assert.equal(emitted.some((event) => event.type === "run.completed"), false);
});
test("browser disconnect aborts the run, finalizes retryable and releases usage", async () => {
const controller = new AbortController();
const aborted = new Promise<void>((resolve) => {
controller.signal.addEventListener("abort", () => resolve(), { once: true });
});
const hanging = (async function* () {
yield chunk("start");
yield chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } });
yield chunk("tool-result", { toolName: "skill" });
yield chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } });
yield chunk("tool-result", { toolName: "rectification-read-case" });
yield chunk("text-delta", { text: "你好," });
// The provider stream hangs until the client disconnects.
await aborted;
})();
const { options, billing } = runOptions({
signal: controller.signal,
buildAgent: async () => ({
stream: async () => ({
fullStream: hanging,
totalUsage: Promise.resolve({ inputTokens: 10, outputTokens: 20 }),
}),
getSkill: async () => ({ name: RECTIFICATION_SKILL_NAME, instructions: "skill" }),
}) as never,
});
const pending = runV9AgentTurn(options);
setTimeout(() => controller.abort(), 30);
const result = await pending;
assert.equal(result.ok, false);
assert.equal(result.errorCode, "stream_aborted");
assert.equal(billing.released, 1);
});
test("empty stream uses server narration instead of retrying the attempt", async () => {
const { options, emitted, billing } = runOptions({
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("finish"),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
assert.match(result.answerText, /已经记下|请继续说下一件/);
assert.equal(billing.completed, 1);
assert.equal(emitted.some((event) => event.type === "attempt.reset"), false);
assert.equal(emitted.some((event) => event.type === "run.completed"), true);
});
test("legacy Skill identity fails before billing reservation", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
get_agentic_rectification_skill_identity: () => {
throw new Error("agentic_rectification_legacy_skill_identity_unverifiable");
},
});
const { options, billing } = runOptions({ accounting: accounting.client });
await assert.rejects(
runV9AgentTurn(options),
(error: unknown) => error instanceof Error
&& error.message.includes("agentic_rectification_legacy_skill_identity_unverifiable"),
);
assert.equal(billing.reserved, 0);
});
test("Skill identity tool failures retain their safe public codes", () => {
assert.equal(
safeToolErrorCode(new Error("agentic_rectification_legacy_skill_identity_unverifiable")),
"legacy_skill_identity_unverifiable",
);
assert.equal(
safeToolErrorCode(new Error("agentic_rectification_skill_identity_missing")),
"skill_identity_missing",
);
});
test("execution receipts are persisted per turn (phases + tools)", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }),
});
const { options } = runOptions({
accounting: accounting.client,
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("text-delta", { text: "你好" }),
chunk("finish"),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
const phases = accounting.calls
.filter((call) => call.fn === "insert_agentic_rectification_run_phase")
.map((call) => call.args.p_phase);
assert.ok(phases.includes("run.started"));
assert.ok(phases.includes("skill.bound"));
assert.ok(phases.includes("case.loaded"));
assert.ok(phases.includes("intent.classified"));
assert.ok(phases.includes("answer.composed"));
assert.ok(phases.includes("billing.settled"));
assert.ok(phases.includes("run.completed"));
// answer.delta and thinking.delta are never persisted per-delta.
assert.ok(!phases.includes("answer.delta"));
assert.ok(!phases.includes("thinking.delta"));
assert.deepEqual(result.toolsUsed, ["rectification-read-case"]);
});
const SECOND_ATTEMPT_ID = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
type AttemptFailure = "stream_aborted" | "stream_unfinished";
function attemptStream(
chunks: StreamChunk[],
usage: { inputTokens: number; outputTokens: number },
) {
return {
stream: async () => ({
fullStream: (async function* () {
for (const item of chunks) yield item;
})(),
totalUsage: Promise.resolve(usage),
}),
getSkill: async () => ({ name: RECTIFICATION_SKILL_NAME, instructions: "skill" }),
};
}
function failedAttemptChunks(errorCode: AttemptFailure): StreamChunk[] {
const chunks: StreamChunk[] = [
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("tool-call", { toolName: "rectification-set-focus", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-set-focus" }),
chunk("text-delta", { text: "失败 attempt 的半截文本" }),
];
if (errorCode === "stream_aborted") {
chunks.push(chunk("error", { error: new Error("provider stream aborted") }));
}
return chunks;
}
function successfulAttemptChunks(): StreamChunk[] {
return [
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("tool-call", { toolName: "rectification-record-evidence-batch", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-record-evidence-batch" }),
chunk("text-delta", { text: "第二次 attempt 成功" }),
chunk("finish"),
];
}
for (const failureCode of ["stream_aborted", "stream_unfinished"] as const) {
test(`${failureCode} attempt is isolated and the second successful attempt exclusively commits public truth`, async () => {
let buildCount = 0;
const completedUsage: Array<{ inputTokens: number; outputTokens: number; durationMs: number }> = [];
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }),
});
const { options, emitted, billing } = runOptions({
accounting: accounting.client,
billing: {
reserve: async () => { billing.reserved += 1; return { success: true, status: 200 }; },
complete: async (usage) => { completedUsage.push(usage); billing.completed += 1; return true; },
release: async () => { billing.released += 1; return true; },
},
buildAgent: async () => {
buildCount += 1;
return buildCount === 1
? attemptStream(failedAttemptChunks(failureCode), { inputTokens: 901, outputTokens: 902 }) as never
: attemptStream(successfulAttemptChunks(), { inputTokens: 31, outputTokens: 17 }) as never;
},
});
const result = await runV9AgentTurn(options);
assert.equal(buildCount, 2);
assert.equal(result.ok, true);
assert.equal(result.answerText, "第二次 attempt 成功");
assert.deepEqual(result.toolsUsed, [
"rectification-read-case",
"rectification-record-evidence-batch",
]);
const resetAt = emitted.findIndex((event) => event.type === "attempt.reset");
assert.ok(resetAt >= 0);
assert.equal(
emitted.some((event) => event.type === "answer.delta" && event.text?.includes("失败 attempt")),
false,
);
assert.deepEqual(
emitted.slice(resetAt + 1).filter((event) => event.type === "answer.delta"),
[{ type: "answer.delta", text: "第二次 attempt 成功" }],
);
assert.equal(
emitted.slice(resetAt + 1).some((event) =>
event.type === "tool.activity"
&& (event as { tool?: string }).tool === "rectification-record-evidence-batch"),
true,
);
assert.deepEqual(completedUsage.map(({ inputTokens, outputTokens }) => ({ inputTokens, outputTokens })), [
{ inputTokens: 31, outputTokens: 17 },
]);
assert.deepEqual(billing, { reserved: 1, completed: 1, released: 0 });
const attemptFinalizations = accounting.calls
.filter((call) => call.fn === "finalize_agentic_rectification_run_attempt")
.map((call) => ({
attemptId: call.args.p_attempt_id,
status: call.args.p_status,
errorCode: call.args.p_error_code,
usage: call.args.p_usage,
}));
assert.deepEqual(attemptFinalizations, [
{
attemptId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
status: "retryable",
errorCode: failureCode,
usage: { inputTokens: 0, outputTokens: 0 },
},
{
attemptId: SECOND_ATTEMPT_ID,
status: "completed",
errorCode: null,
usage: { inputTokens: 31, outputTokens: 17 },
},
]);
const finalizedTurn = accounting.calls.find((call) => call.fn === "finalize_agentic_rectification_turn");
assert.deepEqual(finalizedTurn?.args, {
p_user_id: USER_ID,
p_case_id: CASE_ID,
p_turn_id: TURN_ID,
p_attempt_id: SECOND_ATTEMPT_ID,
p_status: "completed",
p_assistant_message: "第二次 attempt 成功",
p_successful_attempt_id: SECOND_ATTEMPT_ID,
});
const settledPhases = accounting.calls.filter((call) =>
call.fn === "insert_agentic_rectification_run_phase"
&& (call.args.p_phase === "billing.settled" || call.args.p_phase === "run.completed"));
assert.deepEqual(settledPhases.map((call) => call.args.p_attempt_id), [
SECOND_ATTEMPT_ID,
SECOND_ATTEMPT_ID,
]);
const billingSettledIndex = accounting.calls.findIndex((call) =>
call.fn === "insert_agentic_rectification_run_phase"
&& call.args.p_phase === "billing.settled");
const runCompletedIndex = accounting.calls.findIndex((call) =>
call.fn === "insert_agentic_rectification_run_phase"
&& call.args.p_phase === "run.completed");
const successfulAttemptFinalizeIndex = accounting.calls.findIndex((call) =>
call.fn === "finalize_agentic_rectification_run_attempt"
&& call.args.p_status === "completed");
const completedTurnFinalizeIndex = accounting.calls.findIndex((call) =>
call.fn === "finalize_agentic_rectification_turn"
&& call.args.p_status === "completed");
assert.ok(billingSettledIndex < runCompletedIndex);
assert.ok(runCompletedIndex < successfulAttemptFinalizeIndex);
assert.ok(successfulAttemptFinalizeIndex < completedTurnFinalizeIndex);
});
}
test("a failed set-focus does not reset the attempt or hide the terminal answer", async () => {
let buildCount = 0;
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: (_fn, args) => ({
turn_id: TURN_ID,
status: args.p_status,
idempotent: false,
}),
});
const { options, emitted, billing } = runOptions({
accounting: accounting.client,
buildAgent: async () => {
buildCount += 1;
return attemptStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("tool-call", { toolName: "rectification-set-focus", args: { caseId: CASE_ID } }),
chunk("tool-error", { toolName: "rectification-set-focus", error: new Error("invalid_focus") }),
chunk("text-delta", { text: "主问题:请确认这段经历发生在哪个月?" }),
chunk("finish"),
], { inputTokens: 41, outputTokens: 23 }) as never;
},
});
const result = await runV9AgentTurn(options);
assert.equal(buildCount, 1);
assert.equal(result.ok, true);
assert.equal(result.turnStatus, "completed");
assert.equal(result.answerText, "主问题:请确认这段经历发生在哪个月?");
assert.equal(emitted.some((event) => event.type === "attempt.reset"), false);
assert.deepEqual(
emitted.filter((event) => event.type === "answer.delta"),
[{ type: "answer.delta", text: "主问题:请确认这段经历发生在哪个月?" }],
);
assert.equal(
emitted.some((event) => event.type === "tool.activity"
&& (event as { tool?: string; status?: string }).tool === "rectification-set-focus"
&& (event as { tool?: string; status?: string }).status === "failed"
&& (event as { code?: string }).code === "duplicate_focus"),
true,
);
assert.deepEqual(billing, { reserved: 1, completed: 1, released: 0 });
});
test("does not retry set-focus with identical arguments", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: (_fn, args) => ({
turn_id: TURN_ID,
status: args.p_status,
idempotent: false,
}),
});
const { options, emitted, billing } = runOptions({
accounting: accounting.client,
buildAgent: async () => attemptStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("tool-call", { toolName: "rectification-set-focus", args: { caseId: CASE_ID } }),
chunk("tool-error", { toolName: "rectification-set-focus", error: new Error("invalid_focus") }),
chunk("tool-call", { toolName: "rectification-set-focus", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-set-focus" }),
chunk("text-delta", { text: "主问题:请确认这段经历发生在哪个月?" }),
chunk("finish"),
], { inputTokens: 43, outputTokens: 29 }) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
assert.equal(result.errorCode, null);
assert.equal(result.answerText, "主问题:请确认这段经历发生在哪个月?");
assert.equal(emitted.some((event) => event.type === "attempt.reset"), false);
assert.equal(emitted.some((event) => event.type === "run.failed"), false);
assert.deepEqual(billing, { reserved: 1, completed: 1, released: 0 });
});
test("duplicate compare after diagnostics still completes with server narration", async () => {
const executedMethods = [
"ashtakavarga",
"d1-rashi",
"d10-dashamsa",
"shadbala",
"functional-benefic-malefic",
"arudha-pada",
];
const { options, emitted, billing } = runOptions({
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("tool-call", { toolName: "rectification-record-evidence-batch", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-record-evidence-batch" }),
chunk("tool-call", { toolName: "rectification-propose-evidence", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-propose-evidence" }),
chunk("tool-call", { toolName: "rectification-confirm-evidence", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-confirm-evidence" }),
chunk("tool-call", { toolName: "rectification-read-diagnostics", args: { caseId: CASE_ID } }),
chunk("tool-result", {
toolName: "rectification-read-diagnostics",
result: { executed_methods: executedMethods },
}),
chunk("tool-call", { toolName: "rectification-compare-candidates", args: { caseId: CASE_ID } }),
chunk("tool-result", {
toolName: "rectification-compare-candidates",
result: { executed_methods: executedMethods },
}),
chunk("tool-call", { toolName: "rectification-compare-candidates", args: { caseId: CASE_ID } }),
chunk("finish"),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
assert.equal(result.errorCode, null);
assert.match(result.answerText, /已经记下|请继续说下一件/);
assert.equal(emitted.some((event) => event.type === "attempt.reset"), false);
assert.equal(emitted.some((event) => event.type === "run.failed"), false);
assert.equal(emitted.some((event) => event.type === "run.completed"), true);
assert.equal(
emitted.filter((event) => event.type === "tool.activity"
&& (event as { tool?: string; status?: string }).tool === "rectification-compare-candidates"
&& (event as { tool?: string; status?: string }).status === "started").length,
1,
);
assert.deepEqual(billing, { reserved: 1, completed: 1, released: 0 });
});
test("an unclaimed V10 attempt never starts the model", async () => {
let buildCount = 0;
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
create_agentic_rectification_run_attempt: () => ({
attempt_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
status: "started",
should_execute: false,
already_in_progress: true,
idempotent: true,
}),
});
const { options, billing } = runOptions({
accounting: accounting.client,
buildAgent: async () => {
buildCount += 1;
return fakeAgentStream([]) as never;
},
});
await assert.rejects(
runV9AgentTurn(options),
(error: unknown) => error instanceof Error
&& error.message.includes("agentic_rectification_attempt_in_progress"),
);
assert.equal(buildCount, 0);
assert.deepEqual(billing, { reserved: 1, completed: 0, released: 1 });
assert.equal(
accounting.calls.some((call) => call.fn === "finalize_agentic_rectification_run_attempt"),
false,
);
});
test("non-retryable attempt errors do not start a second attempt", async () => {
let buildCount = 0;
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "failed", idempotent: false }),
});
const { options, emitted, billing } = runOptions({
accounting: accounting.client,
buildAgent: async () => {
buildCount += 1;
throw new Error("provider contract violation");
},
});
const result = await runV9AgentTurn(options);
assert.equal(buildCount, 1);
assert.equal(result.ok, false);
assert.equal(result.turnStatus, "failed");
assert.equal(result.errorCode, "run_failed");
assert.deepEqual(billing, { reserved: 1, completed: 0, released: 1 });
assert.deepEqual(emitted, [
{ type: "run.started" },
{
type: "run.failed",
code: "run_failed",
recoverable: false,
message: "上游模型连接失败,状态已记录。",
},
]);
assert.equal(
accounting.calls.filter((call) => call.fn === "create_agentic_rectification_run_attempt").length,
1,
);
const attemptFinalize = accounting.calls.find((call) => call.fn === "finalize_agentic_rectification_run_attempt");
assert.equal(attemptFinalize?.args.p_status, "failed");
assert.equal(attemptFinalize?.args.p_error_code, "run_failed");
const turnFinalize = accounting.calls.find((call) => call.fn === "finalize_agentic_rectification_turn");
assert.equal(turnFinalize?.args.p_attempt_id, attemptFinalize?.args.p_attempt_id);
assert.equal(turnFinalize?.args.p_assistant_message, null);
assert.equal(turnFinalize?.args.p_successful_attempt_id, null);
});
test("thinking-mode tool_choice rejection fails the opening turn without a second identical attempt", async () => {
let buildCount = 0;
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "failed", idempotent: false }),
});
const { options, emitted, billing } = runOptions({
accounting: accounting.client,
action: "opening",
message: null,
buildAgent: async () => {
buildCount += 1;
return {
getSkill: async () => ({ name: RECTIFICATION_SKILL_NAME, instructions: "skill" }),
stream: async () => {
throw new Error("Thinking mode does not support this tool_choice");
},
} as never;
},
});
const result = await runV9AgentTurn(options);
assert.equal(buildCount, 1);
assert.equal(result.ok, false);
assert.equal(result.turnStatus, "failed");
assert.equal(result.errorCode, "thinking_tool_choice_unsupported");
assert.deepEqual(billing, { reserved: 1, completed: 0, released: 1 });
assert.deepEqual(emitted, [
{ type: "run.started" },
{ type: "skill.bound" },
{
type: "run.failed",
code: "thinking_tool_choice_unsupported",
recoverable: false,
message: "本轮没有完成,状态已记录。",
},
]);
assert.equal(
accounting.calls.filter((call) => call.fn === "create_agentic_rectification_run_attempt").length,
1,
);
const attemptFinalize = accounting.calls.find((call) => call.fn === "finalize_agentic_rectification_run_attempt");
assert.equal(attemptFinalize?.args.p_error_code, "thinking_tool_choice_unsupported");
});
test("does not publish intermediate tool-step text as answer.delta", async () => {
const { options, emitted } = runOptions({
message: "2020年4月开始实习 6月转正 10月离职",
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("text-delta", { text: "Let me set" }),
chunk("text-delta", { text: " _probe" }),
chunk("text-delta", { text: " _gain" }),
chunk("tool-call", {
toolName: "rectification-record-evidence-batch",
args: { caseId: CASE_ID },
}),
chunk("tool-result", { toolName: "rectification-record-evidence-batch" }),
chunk("tool-call", { toolName: "rectification-compare-candidates", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-compare-candidates" }),
chunk("text-delta", { text: "记下了,2020年4月开始实习。" }),
chunk("finish"),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
assert.deepEqual(
emitted.filter((event) => event.type === "answer.delta"),
[{ type: "answer.delta", text: "记下了,2020年4月开始实习。" }],
);
const publicText = JSON.stringify(emitted);
assert.doesNotMatch(publicText, /Let me set/);
assert.doesNotMatch(publicText, /_probe/);
assert.doesNotMatch(publicText, /_gain/);
assert.equal(emitted.some((event) => event.type === "thinking.delta"), false);
});
test("Chinese interview planning stays on reasoning-delta; the spoken answer is the model text-delta", async () => {
const spoken = "职业类型已经记下。接下来想请你回想一下这份工作的时间段——**你大概是在哪一年入职的?**只要个大概年份就行。";
const { options, emitted } = runOptions({
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("tool-call", { toolName: "rectification-compare-candidates", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-compare-candidates" }),
chunk("reasoning-delta", {
text: [
"方法覆盖上,职业这一层已经通过 occupation_note 补齐了(不计分)。",
"让我继续访谈,问一件能帮助区分候选的职业前事。",
"本轮对照了Gochara、D1 本命盘、D10 事业分盘。",
].join("\n\n"),
}),
chunk("text-delta", { text: spoken }),
chunk("finish"),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
assert.equal(result.answerText, spoken);
assert.deepEqual(
emitted.filter((event) => event.type === "answer.delta"),
[{ type: "answer.delta", text: spoken }],
);
const publicText = JSON.stringify(emitted);
assert.doesNotMatch(publicText, /occupation_note/);
assert.doesNotMatch(publicText, /让我继续访谈/);
assert.doesNotMatch(publicText, /本轮对照了/);
assert.equal(emitted.some((event) => event.type === "thinking.delta"), false);
});
test("persisted choice prompt replaces a competing model follow-up without a topic denylist", async () => {
const spoken = "好的,2020 年 6 月毕业这条也记下了。\n\n再问你一件:2016 年前后那场重要的入学考试,你当时发挥明显失常、或者压力特别大,有没有发生过?";
const prompt = "2023 年前后,有没有明显入职、升职或职责明显加重?";
assert.equal(
bindSpokenToOpenQuestion(spoken, prompt),
`好的,2020 年 6 月毕业这条也记下了。\n\n${prompt}`,
);
assert.equal(openQuestionPromptFromToolResult({
type: "tool-result",
payload: { result: { open_question: { prompt } } },
}), prompt);
const { options, emitted } = runOptions({
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("tool-call", { toolName: "rectification-record-evidence-batch", args: { caseId: CASE_ID } }),
chunk("tool-result", {
toolName: "rectification-record-evidence-batch",
result: { accepted_count: 1, open_question: { prompt } },
}),
chunk("text-delta", { text: spoken }),
chunk("finish"),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
assert.equal(result.answerText, `好的,2020 年 6 月毕业这条也记下了。\n\n${prompt}`);
assert.doesNotMatch(result.answerText, /入学考试/);
assert.deepEqual(
emitted.filter((event) => event.type === "answer.delta"),
[{ type: "answer.delta", text: result.answerText }],
);
});
test("model terminal text-delta is the reply even when Case narration could be composed", async () => {
const spoken = "职业已经记下。你入职大概是哪一年?说个年份就行。";
const { options, emitted } = runOptions({
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("text-delta", { text: spoken }),
chunk("finish"),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
assert.equal(result.answerText, spoken);
assert.deepEqual(
emitted.filter((event) => event.type === "answer.delta"),
[{ type: "answer.delta", text: spoken }],
);
assert.doesNotMatch(result.answerText, /已经记下:/);
assert.doesNotMatch(result.answerText, /2016年9月离家去北京开始工作/);
});
test("does not reset the whole attempt after duplicate_focus", async () => {
let buildCount = 0;
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: (_fn, args) => ({
turn_id: TURN_ID,
status: args.p_status,
idempotent: false,
}),
});
const { options, emitted } = runOptions({
accounting: accounting.client,
buildAgent: async () => {
buildCount += 1;
return attemptStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("tool-call", { toolName: "rectification-record-evidence-batch", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-record-evidence-batch" }),
chunk("tool-call", { toolName: "rectification-set-focus", args: { caseId: CASE_ID } }),
chunk("tool-error", { toolName: "rectification-set-focus", error: new Error("duplicate_focus") }),
chunk("text-delta", { text: "已经记下实习,接下来对一下考试那年。" }),
chunk("finish"),
], { inputTokens: 11, outputTokens: 12 }) as never;
},
});
const result = await runV9AgentTurn(options);
assert.equal(buildCount, 1);
assert.equal(result.ok, true);
assert.equal(emitted.some((event) => event.type === "attempt.reset"), false);
assert.equal(
result.toolsUsed.filter((name) => name === "rectification-record-evidence-batch").length,
1,
);
});
test("persists message origin for every user turn", async () => {
const message = "2020年4月开始实习 6月转正 10月离职";
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }),
});
const { options } = runOptions({
accounting: accounting.client,
message,
messageOrigin: "typed",
clientActionId: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("text-delta", { text: "记下了,2020年4月开始实习。" }),
chunk("finish"),
]) as never,
});
await runV9AgentTurn(options);
const origin = accounting.calls.find((call) => call.fn === "record_agentic_rectification_turn_origin");
assert.deepEqual(origin?.args, {
p_user_id: USER_ID,
p_case_id: CASE_ID,
p_turn_id: TURN_ID,
p_origin: "typed",
p_client_action_id: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
p_content_hash: messageContentHash(message),
});
});
test("does not create a 2002 user message from a 2020 assistant suggestion", async () => {
const message = "2020年4月开始实习 6月转正 10月离职";
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }),
});
const { options, emitted } = runOptions({
accounting: accounting.client,
message,
messageOrigin: "typed",
buildAgent: async () => fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("text-delta", { text: "202" }),
chunk("text-delta", { text: "0" }),
chunk("text-delta", { text: " Let me ask about 2002" }),
chunk("tool-call", { toolName: "rectification-compare-candidates", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-compare-candidates" }),
chunk("text-delta", { text: "记下了,2020年4月开始实习。" }),
chunk("finish"),
]) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
const origin = accounting.calls.find((call) => call.fn === "record_agentic_rectification_turn_origin");
assert.equal(origin?.args.p_origin, "typed");
assert.equal(origin?.args.p_content_hash, messageContentHash(message));
const publicText = JSON.stringify(emitted);
assert.doesNotMatch(publicText, /2002 年发生什么了/);
assert.doesNotMatch(result.answerText, /2002/);
assert.equal(result.answerText, "记下了,2020年4月开始实习。");
});