fix(rectification): bind skill before provider run
This commit is contained in:
@@ -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"),
|
||||
}];
|
||||
|
||||
@@ -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;无法唯一指向时只做简短澄清,不得猜测。
|
||||
|
||||
Reference in New Issue
Block a user