feat(rectification): add adaptive director tool loop
This commit is contained in:
@@ -4,7 +4,7 @@ import test from "node:test";
|
||||
import {
|
||||
canRegenerateRectificationMessage,
|
||||
rectificationV4ChatMessages,
|
||||
rectificationPhaseLabel,
|
||||
rectificationPhaseLabel, rectificationProgressLabel,
|
||||
toggleRectificationFeedback,
|
||||
} from "../src/components/rectification-v4-panel.tsx";
|
||||
import { applyRectificationV4JobUpdate } from "../src/hooks/use-rectification-v4.ts";
|
||||
@@ -259,9 +259,13 @@ test("processing follows every server job phase returned by polling", () => {
|
||||
const message = rectificationV4ChatMessages(data, true).at(-1);
|
||||
assert.equal(message?.role, "assistant");
|
||||
assert.equal(message?.state, "thinking");
|
||||
assert.equal(message?.text, label);
|
||||
assert.equal(message?.text, rectificationProgressLabel(phase));
|
||||
assert.match(message?.text ?? "", new RegExp(label));
|
||||
}
|
||||
assert.equal(rectificationPhaseLabel("checking_robustness"), "正在检查候选范围的稳定性…");
|
||||
assert.equal(rectificationProgressLabel("extracting_evidence"), "正在整理你刚才提到的经历…\n正在分析:\n● 整理已确认事件\n○ 比较候选时间差异\n○ 确定下一步验证方向");
|
||||
assert.equal(rectificationProgressLabel("checking_robustness"), "正在检查候选范围的稳定性…\n正在分析:\n✓ 整理已确认事件\n● 比较候选时间差异\n○ 确定下一步验证方向");
|
||||
assert.equal(rectificationProgressLabel("reasoning"), "正在选择下一步动作…\n正在分析:\n✓ 整理已确认事件\n✓ 比较候选时间差异\n● 确定下一步验证方向");
|
||||
});
|
||||
|
||||
test("polling ignores an older job response so the visible phase cannot move backward", () => {
|
||||
|
||||
@@ -441,6 +441,15 @@ test("V6 迁移只更新未完成 Case 版本且不写 active_birth_time", () =>
|
||||
assert.doesNotMatch(migration, /profiles\s*\.\s*active_birth_time|active_birth_time/i);
|
||||
});
|
||||
|
||||
test("V8 Director migration advances only unfinished Agent cases", () => {
|
||||
const migration = readFileSync(new URL("../supabase/migrations/20260731020000_rectification_director_v4_runtime.sql", import.meta.url), "utf8");
|
||||
assert.match(migration, /alter column skill_version set default 'birth-time-rectification-v8'/);
|
||||
assert.match(migration, /alter column prompt_version set default 'rectification-director-v4'/);
|
||||
assert.match(migration, /where status in \('awaiting_answer', 'processing', 'paused'\)/);
|
||||
assert.match(migration, /deployment_mode in \('v5_shadow', 'v5_agent'\)/);
|
||||
assert.doesNotMatch(migration, /profiles\s*\.\s*active_birth_time|active_birth_time/i);
|
||||
});
|
||||
|
||||
test("新事件机会提供具体回忆线索和退出方式,不把离家上大学换词重问成迁居", () => {
|
||||
const university = event({ summary: "离家去外地上大学", rawText: "2016年9月离家去外地上大学" });
|
||||
const opportunities = buildQuestionOpportunities({
|
||||
|
||||
@@ -38,8 +38,8 @@ const caseValue: RectificationV4Case = {
|
||||
latestSnapshot: null,
|
||||
orchestrationModelId: null,
|
||||
narrationModelId: null,
|
||||
skillVersion: "birth-time-rectification-v6",
|
||||
promptVersion: "rectification-director-v1",
|
||||
skillVersion: "birth-time-rectification-v8",
|
||||
promptVersion: "rectification-director-v4",
|
||||
algorithmVersion: "rectification-v5-matrix-scoring-1",
|
||||
deploymentMode: "v5_agent",
|
||||
agentMode: "agent",
|
||||
@@ -319,7 +319,7 @@ test("explicit subject correction is grounded and enters pending review", () =>
|
||||
assert.equal(staged.revisions[0]?.scoreability, "pending_review");
|
||||
});
|
||||
|
||||
test("declined targets cannot be reopened and diagnostics stay in a bounded tool loop", async () => {
|
||||
test("declined targets cannot be reopened and the Director adapts through server-owned observations", async () => {
|
||||
const target = event({ eventId: "00000000-0000-4000-8000-000000000706" });
|
||||
const targetDossier = buildRectificationCaseDossier({ caseValue, turns: [], events: [target], snapshot: null, diagnostics: null, targetDisposition: "declined", currentTargetEventId: target.eventId });
|
||||
const reopened = plan({ targetDisposition: "declined", action: { type: "ask_question", focus: { mode: "clarify_existing_event", targetEventId: target.eventId, domain: target.domain, requestedFacts: ["month"], rationaleCodes: ["retry"] }, question: "再说说那件事?", optionalQuickReplies: [] } });
|
||||
@@ -327,6 +327,55 @@ test("declined targets cannot be reopened and diagnostics stay in a bounded tool
|
||||
const reopenedWithoutId = plan({ targetDisposition: "declined", action: { type: "ask_question", focus: { mode: "clarify_existing_event", targetEventId: null, domain: target.domain, requestedFacts: ["month"], rationaleCodes: ["retry"] }, question: "再说说那件事?", optionalQuickReplies: [] } });
|
||||
assert.ok(validateRectificationTurnPlan({ plan: reopenedWithoutId, dossier: targetDossier, latestAnswer: "不想说", phase: "final" }).issues.includes("declined_target_reopened"));
|
||||
|
||||
const phases: string[] = [];
|
||||
const prompts: string[] = [];
|
||||
const requests = [
|
||||
{ tool: "case_read", diagnostic: null },
|
||||
{ tool: "candidate_scan", diagnostic: null },
|
||||
{ tool: "evidence_gap", diagnostic: null },
|
||||
{ tool: "diagnostic_read", diagnostic: "candidate_split" },
|
||||
] as const;
|
||||
const result = await runRectificationDirector({
|
||||
caseValue,
|
||||
dossier: dossier(),
|
||||
latestAnswer: "",
|
||||
phase: "final",
|
||||
diagnostics,
|
||||
generatePlan: async (prompt, phase) => {
|
||||
phases.push(phase);
|
||||
prompts.push(prompt);
|
||||
const request = requests[phases.length - 1];
|
||||
return { object: request ? plan({ action: { type: "request_tool", ...request } }) : plan() };
|
||||
},
|
||||
});
|
||||
assert.equal(result.mode, "agent");
|
||||
assert.deepEqual(phases, ["final", "after_observation", "after_observation", "after_observation", "after_observation"]);
|
||||
assert.deepEqual(result.toolCalls.map(({ tool, diagnostic }) => [tool, diagnostic]), [
|
||||
["case_read", null],
|
||||
["candidate_scan", null],
|
||||
["evidence_gap", null],
|
||||
["diagnostic_read", "candidate_split"],
|
||||
]);
|
||||
assert.equal(dossier().capabilities.maxToolRounds, 10);
|
||||
const firstPrompt = JSON.parse(prompts[0]!);
|
||||
assert.equal(firstPrompt.dossier.eventLedger, undefined);
|
||||
assert.equal(firstPrompt.dossier.candidateState, undefined);
|
||||
assert.equal(firstPrompt.dossier.runtime.hypotheses, undefined);
|
||||
assert.deepEqual(firstPrompt.dossier.availableTools.readOnly, ["case_read", "candidate_scan", "evidence_gap"]);
|
||||
const caseObservationPrompt = JSON.parse(prompts[1]!);
|
||||
assert.ok(Array.isArray(caseObservationPrompt.latestObservation.result.eventLedger));
|
||||
assert.equal(caseObservationPrompt.dossier.runtime.observations[0].tool, "case_read");
|
||||
const candidateObservationPrompt = JSON.parse(prompts[2]!);
|
||||
assert.equal(candidateObservationPrompt.latestObservation.result.candidateState.hasSnapshot, false);
|
||||
assert.ok(Array.isArray(candidateObservationPrompt.latestObservation.result.hypotheses));
|
||||
const finalPrompt = JSON.parse(prompts[4]!);
|
||||
assert.equal(finalPrompt.dossier.runtime.revision, 4);
|
||||
assert.deepEqual(finalPrompt.dossier.runtime.observations.map((item: { tool: string }) => item.tool), requests.map(({ tool }) => tool));
|
||||
assert.equal(result.dossier.runtime.revision, 4);
|
||||
assert.equal(result.plan.action.type, "ask_question");
|
||||
});
|
||||
|
||||
test("repeated immutable tools are not executed twice and force one convergence decision", async () => {
|
||||
const phases: string[] = [];
|
||||
const prompts: string[] = [];
|
||||
const result = await runRectificationDirector({
|
||||
@@ -338,13 +387,18 @@ test("declined targets cannot be reopened and diagnostics stay in a bounded tool
|
||||
generatePlan: async (prompt, phase) => {
|
||||
phases.push(phase);
|
||||
prompts.push(prompt);
|
||||
return { object: phases.length <= 2 ? plan({ action: { type: "request_diagnostic", diagnostic: phases.length === 1 ? "candidate_split" : "date_sensitivity" } }) : plan() };
|
||||
return { object: phase === "converge" ? plan() : plan({ action: { type: "request_tool", tool: "candidate_scan", diagnostic: null } }) };
|
||||
},
|
||||
});
|
||||
assert.equal(result.mode, "agent");
|
||||
assert.deepEqual(phases, ["final", "after_diagnostic", "after_diagnostic"]);
|
||||
assert.equal(result.toolCalls.length, 2);
|
||||
assert.deepEqual(JSON.parse(prompts[2]!).diagnosticResults.map((item: { diagnostic: string }) => item.diagnostic), ["candidate_split", "date_sensitivity"]);
|
||||
assert.deepEqual(phases, ["final", "after_observation", "converge"]);
|
||||
assert.equal(result.toolCalls.length, 1);
|
||||
assert.deepEqual(JSON.parse(prompts[2]!).loopState, {
|
||||
round: 1,
|
||||
maxRounds: 10,
|
||||
observedTools: ["candidate_scan:"],
|
||||
convergenceReason: "tool_repeated",
|
||||
});
|
||||
assert.equal(result.plan.action.type, "ask_question");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user