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
@@ -10,6 +10,7 @@ import type { V9CaseDossier } from "./tool-service";
|
||||
import { cachedSystemMessage } from "../../agent-generation-settings.ts";
|
||||
import { OPENING_COLLECT_DOMAINS } from "../user-copy";
|
||||
import { retryConstraintForAttempt } from "./host-fallback";
|
||||
import { sliceRectificationSkill } from "./skill-slice";
|
||||
import type { V9AgentRunOptions } from "./agent-run";
|
||||
|
||||
function clockWindow(range: { start_time?: string | null; end_time?: string | null } | null | undefined): string | null {
|
||||
@@ -54,9 +55,12 @@ export function buildAgentMessages(
|
||||
const timeContext = options.timeContext
|
||||
?? `服务端当前时间(权威):${new Date().toISOString()}。涉及“现在、今天、今年、未来几个月”等相对时间时,以此为准。`;
|
||||
const caseContext = `【服务端 Case ID】${options.caseId}。所有 rectification 工具调用的 caseId 必须原样使用此值。`;
|
||||
// T6 (TASK-rectification-grounding-20260927): an evidence turn gets only
|
||||
// the Skill sections it uses; other actions keep the whole bound body.
|
||||
const boundSkill = sliceRectificationSkill(skillInstructions, options.action).text;
|
||||
const bootstrapContent = [
|
||||
"【服务器已绑定当前 Case 的精确 Skill】运行器已在本 attempt 内加载并核验下列指令;不要重复调用 skill。第一步必须调用 rectification-read-case。",
|
||||
skillInstructions,
|
||||
boundSkill,
|
||||
...(attempt > 1 ? [retryConstraintForAttempt(previousErrorCode)] : []),
|
||||
].join("\n\n");
|
||||
const bootstrap = cachedSystemMessage(bootstrapContent, options.generationModel)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Per-turn slice of the bound rectification Skill (TASK-rectification-
|
||||
* grounding-20260927 T6).
|
||||
*
|
||||
* The runner used to send the whole bound SKILL.md body (about 10.7K
|
||||
* characters for 10.0.31) on every model call. An evidence turn only uses
|
||||
* §5 (ConversationFocus and intent) and §7 (batch evidence and date
|
||||
* truthfulness); the rest describes the opening, case status table, fields
|
||||
* that only exist in `full_diagnostics` (method_followup_plan,
|
||||
* CaseConversationSummary, guided_collect_windows …), the candidate language
|
||||
* of offer turns, and upstream sync. Other actions keep the whole body.
|
||||
*
|
||||
* The slice is keyed on `## N. <title>` heading titles, not on numbers:
|
||||
* the 9.0.0 snapshot numbers its sections differently (its §5 is the
|
||||
* candidate language). If any wanted title is missing, the whole body is
|
||||
* sent unchanged, so a historical Case still runs with its exact bound Skill
|
||||
* (BUG-621). All 10.0.x snapshots share the 10.0.31 headings.
|
||||
*/
|
||||
import type { RectificationAgentAction } from "./step-budget";
|
||||
|
||||
/** Heading titles per action; evidence = §5 and §7 of the 10.0.x layout. */
|
||||
export const RECTIFICATION_SKILL_SECTIONS_BY_ACTION: Readonly<
|
||||
Record<RectificationAgentAction, readonly string[] | "all">
|
||||
> = {
|
||||
opening: "all",
|
||||
read_only: "all",
|
||||
evidence: ["ConversationFocus 与意图承接", "批量证据与日期真实性"],
|
||||
rescore: "all",
|
||||
accept: "all",
|
||||
confirm: "all",
|
||||
};
|
||||
|
||||
export type RectificationSkillSlice = Readonly<{
|
||||
text: string;
|
||||
/** Heading lines sent, or "all" when the whole body went out. */
|
||||
sections: readonly string[] | "all";
|
||||
}>;
|
||||
|
||||
function headingTitle(line: string): string | null {
|
||||
const match = /^##\s+(?:\d+\.\s*)?(.+?)\s*$/.exec(line);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
export function sliceRectificationSkill(
|
||||
instructions: string,
|
||||
action: RectificationAgentAction,
|
||||
): RectificationSkillSlice {
|
||||
const wanted = RECTIFICATION_SKILL_SECTIONS_BY_ACTION[action];
|
||||
if (wanted === "all") return { text: instructions, sections: "all" };
|
||||
const preamble: string[] = [];
|
||||
const sections: Array<{ title: string; lines: string[] }> = [];
|
||||
for (const line of instructions.split("\n")) {
|
||||
const title = line.startsWith("## ") ? headingTitle(line) : null;
|
||||
if (title !== null) {
|
||||
sections.push({ title, lines: [line] });
|
||||
continue;
|
||||
}
|
||||
if (sections.length > 0) sections.at(-1)!.lines.push(line);
|
||||
else preamble.push(line);
|
||||
}
|
||||
const picked = wanted.map((title) => sections.find((section) => section.title === title));
|
||||
if (picked.some((section) => !section)) return { text: instructions, sections: "all" };
|
||||
const title = preamble.find((line) => line.startsWith("# ")) ?? "";
|
||||
const headings = picked.map((section) => section!.lines[0]!.trim());
|
||||
const note = `(本轮是证据轮:以下只附本轮适用的 Skill 章节「${picked.map((section) => section!.title).join("」「")}」;其余章节不适用于本轮,按服务器投影与工具说明执行。)`;
|
||||
const body = picked.map((section) => section!.lines.join("\n").trim()).join("\n\n");
|
||||
return {
|
||||
text: [title, note, body].filter(Boolean).join("\n\n"),
|
||||
sections: headings,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Agent } from "@mastra/core/agent";
|
||||
import type { Processor } from "@mastra/core/processors";
|
||||
import type { ResolvedLanguageModel } from "./model";
|
||||
import {
|
||||
resolveActiveSkillPackage,
|
||||
@@ -56,8 +57,30 @@ export function getRectificationV9Agent(
|
||||
model: model.model,
|
||||
instructions: agenticRectificationInstructions,
|
||||
skills: [resolveSkillPackageRuntimePath(skillPackage)],
|
||||
inputProcessors: [rectificationSkillBoundProcessor],
|
||||
tools: createRectificationV9AgentTools(ctx),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* R4 (TASK-rectification-grounding-20260927): Mastra's default skills
|
||||
* processor added an `<available_skills>` block (with a temporary package
|
||||
* path) and "call the `skill` tool" to every step, contradicting the runner's
|
||||
* "do not call skill" and costing about 1K characters per call.
|
||||
* `providesSkillDiscovery: "on-demand"` declares that the caller owns skill
|
||||
* loading, which is true: the runner loads the exact bound Skill through
|
||||
* `agent.getSkill` and quotes it (sliced per turn) in the bootstrap message.
|
||||
* With this marker Mastra adds neither the system block nor the `skill` /
|
||||
* `skill_search` tools (same mechanism as the consultation agents'
|
||||
* `jyotishSkillBoundProcessor`).
|
||||
*/
|
||||
export const rectificationSkillBoundProcessor: Processor & { processInputStep: NonNullable<Processor["processInputStep"]> } = {
|
||||
id: "rectification-skill-bound",
|
||||
name: "Rectification Skill Bound",
|
||||
providesSkillDiscovery: "on-demand",
|
||||
processInputStep() {
|
||||
// Nothing to inject: the bound Skill text rides in the runner's bootstrap message.
|
||||
},
|
||||
};
|
||||
|
||||
export { RECTIFICATION_V9_SKILL_NAME };
|
||||
|
||||
Reference in New Issue
Block a user