perf(rectification): evidence turns get Skill §5/§7 only; drop Mastra <available_skills> injection (R4)
T6 of TASK-rectification-grounding-20260927. - skill-slice.ts: per-action slice rule as a code constant, keyed on heading titles (evidence: 「ConversationFocus 与意图承接」「批量证据与日期真实性」, i.e. §5/§7 of the 10.0.x layout); other actions and any bound Skill without those headings (9.0.0) get the whole body, so historical Cases still run with their exact bound Skill (BUG-621). - The rectification Agent declares providesSkillDiscovery "on-demand" (rectificationSkillBoundProcessor): no <available_skills> block with a temp path and no "call the skill tool" system message; getSkill still loads the bound package. - Measured on a real Agent + recording model (public AA case, estimate = CJK chars + other chars / 4): fixed overhead per call 12,002 → 6,471 tokens (step 0: 8,440 → 2,909). Skill text unchanged; no version bump. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1ed3e55579
commit
c4bb394bbf
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* 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<string, unknown> };
|
||||
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 <available_skills> 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, /<available_skills>/);
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user