/** * TASK-rectification-grounding-20260927 T6 (R4 + Skill slicing): what a real * rectification Agent sends the model on an evidence turn. Real * `runV9AgentTurn` + real `getRectificationV9Agent` (real bound Skill, real * tools) + a prompt-recording scripted model; case data is a real local * engine response for a public AA chart (9 candidates). * * Token estimate (for the size table): one token per CJK character plus one * per four other characters (`estimateTokens`). "Fixed overhead" is what every * call of the turn re-sends regardless of tool results: the system messages * plus the tool schemas. */ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { CASE_ID, SESSION_ID, TURN_ID, USER_ID, dossierFixture, fakeAccounting, receiptHandlers, } from "./rectification-v9-test-support.ts"; import { AA_EVIDENCE_ROWS, AA_RANGE, AA_TURNS, buildGoldenLatest, contentText, estimateTokens, scriptedModel, setFocusHandler, stubGoldenFetch, type RecordedCall, } from "./rectification-grounding-support.ts"; import { getRectificationV9Agent } from "../src/mastra/agentic-rectification.ts"; import { runV9AgentTurn } from "../src/lib/rectification-agentic/v9/agent-run.ts"; import { RECTIFICATION_SKILL_SECTIONS_BY_ACTION, sliceRectificationSkill, } from "../src/lib/rectification-agentic/v9/skill-slice.ts"; import { resolveActiveSkillPackage, resolveExactSkillPackage, } from "../src/lib/skill-package-registry.ts"; import { resolve } from "node:path"; export type CallSize = Readonly<{ call: number; systemChars: number; systemTokens: number; toolsBytes: number; toolsTokens: number; toolNames: string[]; fixedTokens: number; totalTokens: number; }>; export function callSizes(calls: readonly RecordedCall[]): CallSize[] { return calls.map((call, index) => { const system = call.prompt.filter((message) => message.role === "system").map((message) => contentText(message.content)).join("\n"); const tools = JSON.stringify(call.tools ?? []); const all = call.prompt.map((message) => contentText(message.content)).join("\n"); return { call: index + 1, systemChars: system.length, systemTokens: estimateTokens(system), toolsBytes: Buffer.byteLength(tools, "utf8"), toolsTokens: estimateTokens(tools), toolNames: Array.isArray(call.tools) ? (call.tools as Array<{ name: string }>).map((tool) => tool.name) : [], fixedTokens: estimateTokens(system) + estimateTokens(tools), totalTokens: estimateTokens(all) + estimateTokens(tools), }; }); } /** One evidence turn: read-case, then one short body (no write claimed). */ export async function recordEvidenceTurn() { const { latest, compute } = await buildGoldenLatest(); const { model, calls } = scriptedModel([ { parts: [{ tool: { name: "rectification-read-case", input: { caseId: CASE_ID } } }], finish: "tool-calls" }, { parts: [{ text: "2014 年 8 月结婚这件事拿去和星盘对照了。" }], finish: "stop" }, ]); const active = resolveActiveSkillPackage("jyotish-birth-time-rectification"); const accounting = fakeAccounting({ ...receiptHandlers, // The case is bound to the active Skill (10.0.x layout), as a new case is. get_agentic_rectification_skill_identity: () => ({ skill_name: active.name, skill_version: active.version, skill_sha256: active.sha256, skill_source_commit: active.sourceCommit, }), insert_agentic_rectification_skill_run_receipt: () => ({ receipt_id: "88888888-8888-4888-8888-888888888888", skill_name: active.name, skill_version: active.version, skill_sha256: active.sha256, source_commit: active.sourceCommit, }), get_agentic_rectification_case_dossier: () => { const dossier = dossierFixture({ evidence: AA_EVIDENCE_ROWS, latestResult: latest, turns: AA_TURNS as never, candidateRange: AA_RANGE as never, }) as { case: Record }; return { ...dossier, case: { ...dossier.case, skill_version: active.version } }; }, get_agentic_rectification_case_compute: () => compute, set_agentic_rectification_conversation_focus: setFocusHandler, append_agentic_rectification_turn: () => ({ turn_id: TURN_ID, should_execute: true, status: "pending" }), finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }), }, { fallback: () => null }); const restore = stubGoldenFetch(); try { await runV9AgentTurn({ userId: USER_ID, caseId: CASE_ID, sessionId: SESSION_ID, requestId: "req-grounding-prompt", action: "evidence", message: "2014 年 8 月结婚。", modelName: "fake", accounting: accounting.client as never, billing: { reserve: async () => ({ success: true, status: 200 }), complete: async () => true, release: async () => true }, emit: () => {}, expectedWrite: "none", buildAgent: async (turnId, skillPackage, attemptId) => getRectificationV9Agent( { id: "fake", label: "fake", description: "", creditCost: 1, isDefault: true, mode: "compatible", model: model as never } as never, { userId: USER_ID, caseId: CASE_ID, turnId, attemptId, userMessage: "2014 年 8 月结婚。", accounting: accounting.client as never }, skillPackage, ), }); } finally { restore(); } return calls; } function activeSkillBody(): string { const skill = resolveActiveSkillPackage("jyotish-birth-time-rectification"); const raw = readFileSync(resolve(skill.resolvedPath, "SKILL.md"), "utf8"); return raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim(); } test("R4: an evidence-turn call carries no block and no skill tools", async () => { const calls = await recordEvidenceTurn(); assert.ok(calls.length >= 2); for (const call of calls) { const system = call.prompt.filter((message) => message.role === "system").map((message) => contentText(message.content)).join("\n"); assert.doesNotMatch(system, //); assert.doesNotMatch(system, /Skills are NOT tools|call the `skill` tool/); const names = Array.isArray(call.tools) ? (call.tools as Array<{ name: string }>).map((tool) => tool.name) : []; assert.equal(names.includes("skill"), false); assert.equal(names.includes("skill_search"), false); } }); test("T6: the evidence-turn bootstrap carries Skill §5 and §7 only", async () => { const calls = await recordEvidenceTurn(); const system = calls[0]!.prompt.filter((message) => message.role === "system").map((message) => contentText(message.content)).join("\n"); assert.match(system, /## 5\. ConversationFocus 与意图承接/); assert.match(system, /## 7\. 批量证据与日期真实性/); for (const heading of ["## 1. ", "## 2. ", "## 3. ", "## 4. ", "## 6. ", "## 8. ", "## 9. ", "## 10. ", "## 11. "]) { assert.equal(system.includes(heading), false, `${heading} in ${system.slice(system.indexOf(heading) - 200, system.indexOf(heading) + 200)}`); } // full_diagnostics-only fields described in §6 are not in the evidence turn. assert.doesNotMatch(system, /guided_collect_windows|method_followup_plan/); assert.match(system, /本轮是证据轮/); assert.match(system, /# Jyotish 生时校正(V10)/); }); test("T6: fixed overhead per evidence-turn call is under 8K estimated tokens", async () => { const sizes = callSizes(await recordEvidenceTurn()); console.log(JSON.stringify({ scope: "T6 evidence turn sizes", sizes })); for (const size of sizes) assert.ok(size.fixedTokens < 8_000, JSON.stringify(size)); }); test("T6: the slice rule is a code constant; other actions and unknown layouts get the whole Skill", () => { assert.deepEqual(RECTIFICATION_SKILL_SECTIONS_BY_ACTION.evidence, ["ConversationFocus 与意图承接", "批量证据与日期真实性"]); assert.equal(RECTIFICATION_SKILL_SECTIONS_BY_ACTION.opening, "all"); assert.equal(RECTIFICATION_SKILL_SECTIONS_BY_ACTION.read_only, "all"); const body = activeSkillBody(); assert.equal(sliceRectificationSkill(body, "opening").text, body); const evidence = sliceRectificationSkill(body, "evidence"); assert.deepEqual(evidence.sections, ["## 5. ConversationFocus 与意图承接", "## 7. 批量证据与日期真实性"]); assert.ok(evidence.text.length < body.length / 2); const oldLayout = "# 旧版\n\n## 概述\n\n没有编号的章节。"; assert.equal(sliceRectificationSkill(oldLayout, "evidence").text, oldLayout); // BUG-621: the oldest deprecated snapshot still resolves and still slices safely. const oldest = resolveExactSkillPackage( "jyotish-birth-time-rectification", "9.0.0", "5acb3103e80993ea611b93d2c1746e70b74aff8f8636bcffc80a837b954b470d", ); const oldestBody = readFileSync(resolve(oldest.resolvedPath, "SKILL.md"), "utf8"); // 9.0.0 numbers its sections differently (its §5 is the candidate language): // no title match, so the whole bound body goes out unchanged. assert.equal(sliceRectificationSkill(oldestBody, "evidence").text, oldestBody); });