fix(rectification): bind skill before provider run
Independent Staging Quality Gate / validate (push) Successful in 15m7s
Independent Staging Quality Gate / publish (push) Successful in 9m36s

This commit is contained in:
Jesse_Chen
2026-08-16 15:31:00 +08:00
parent 4f6cf5782d
commit 9df43e8112
4 changed files with 131 additions and 46 deletions
@@ -425,14 +425,22 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
return failedAttempt(attemptId, "skill_not_loaded");
}
const messages = buildAgentMessages(options, attemptNumber, dossier);
const rawSkillInstructions = (frameworkSkill as { instructions?: unknown }).instructions;
const skillInstructions = typeof rawSkillInstructions === "string"
? rawSkillInstructions.trim()
: "";
if (!skillInstructions) {
return failedAttempt(attemptId, "skill_not_loaded");
}
const messages = buildAgentMessages(options, attemptNumber, dossier, skillInstructions);
const maxSteps = resolveRectificationStepBudget(action);
const abortController = new AbortController();
const onAbort = () => abortController.abort();
signal?.addEventListener("abort", onAbort, { once: true });
const timeout = setTimeout(() => abortController.abort(), 105_000);
let skillBound = false;
let skillBound = true;
let caseLoaded = false;
let intentClassified = false;
let streamFailed = false;
@@ -457,10 +465,30 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
try {
await recordPhase("run.started");
await insertV9SkillRunReceipt(
accounting,
userId,
caseId,
turnId,
attemptId,
"turn",
skillPackage,
);
await recordPhase("skill.bound");
events.push({ type: "skill.bound" });
emittedKeys.add("event:skill.bound::");
const result = await (agent as unknown as {
stream(
messages: unknown[],
streamOptions: { maxSteps: number; abortSignal: AbortSignal; instructions?: string },
streamOptions: {
maxSteps: number;
abortSignal: AbortSignal;
prepareStep: (input: { stepNumber: number }) => {
activeTools: string[];
toolChoice: { type: "tool"; toolName: string };
} | undefined;
},
): Promise<{
fullStream: AsyncIterable<{
type: string;
@@ -472,9 +500,12 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
}).stream(messages, {
maxSteps,
abortSignal: abortController.signal,
...(attemptNumber > 1 ? {
instructions: "严格按运行合同执行:先加载绑定 Skill,再读取 Case;不得复用上一次 attempt 的文本或工具状态。",
} : {}),
prepareStep: ({ stepNumber }) => stepNumber === 0
? {
activeTools: ["rectification-read-case"],
toolChoice: { type: "tool", toolName: "rectification-read-case" },
}
: undefined,
});
for await (const chunk of result.fullStream) {
@@ -503,7 +534,7 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
}
const phaseEvent = mapStreamChunkToPhase(chunk as never);
if (phaseEvent) {
if (phaseEvent.type === "skill.bound") {
if (phaseEvent.type === "skill.bound" && !skillBound) {
skillBound = true;
await insertV9SkillRunReceipt(
accounting,
@@ -662,20 +693,30 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
function buildAgentMessages(
options: V9AgentRunOptions,
_attempt: number,
attempt: number,
dossier: V9CaseDossier,
skillInstructions: string,
): unknown[] {
void _attempt;
const timeContext = options.timeContext
?? `服务端当前时间(权威):${new Date().toISOString()}。涉及“现在、今天、今年、未来几个月”等相对时间时,以此为准。`;
const caseContext = `【服务端 Case ID】${options.caseId}。所有 rectification 工具调用的 caseId 必须原样使用此值。`;
const bootstrap = {
role: "system",
content: [
"【服务器已绑定当前 Case 的精确 Skill】运行器已在本 attempt 内加载并核验下列指令;不要重复调用 skill。第一步必须调用 rectification-read-case。",
skillInstructions,
...(attempt > 1
? ["【重试约束】不得复用上一次 attempt 的文本或工具状态;从 rectification-read-case 重新读取服务器事实。"]
: []),
].join("\n\n"),
};
if (options.action === "opening") {
return [{
return [bootstrap, {
role: "user",
content: [timeContext, caseContext, openingBrief(dossier)].join("\n"),
}];
}
return [{
return [bootstrap, {
role: "user",
content: [timeContext, caseContext, options.message ?? ""].join("\n"),
}];
+1 -1
View File
@@ -61,7 +61,7 @@ export function resolveRectificationStepBudget(action: RectificationAgentAction)
const agenticRectificationInstructions = `你是 Jyotisha,只服务当前绑定 jyotish-birth-time-rectification Skill 的生时校正 Case。方法、OpeningPolicy、ConversationFocus、长会话摘要、批量证据和候选比较策略全部以本 Case 绑定的不可变 Skill 为准,不在系统提示中重写。
硬性运行与安全边界:
1. 每轮必须先加载 Case 绑定的精确 Skill 包,再调用 rectification-read-case运行器会阻止在此之前执行其他校正动作。
1. 运行器会在每个 attempt 开始前加载并核验 Case 绑定的精确 Skill 包;你不要重复调用 skill,第一步直接调用 rectification-read-case运行器会阻止在读取 Case 前执行其他校正动作。
2. 服务器是 Case、ConversationFocus、CaseConversationSummary、Evidence、Candidate、Turn、Receipt、计费、ownership 与终态的唯一权威。只使用工具返回的当前状态,不从旧正文猜测目标或事实。
3. 事实只能来自用户原话;不得虚构或补全事件、日期、人物关系、动机、分盘、评分、候选或出生分钟。日期精度按用户真实表达保留。
4. 工具只传最小引用。承接、拒答、确认和修订必须引用服务器返回且仍 active 的 focusId/evidenceId;无法唯一指向时只做简短澄清,不得猜测。