fix(rectification): run adopt narration on the last discriminator click
The early-exit reused the pre-click dossier, so the agent never ran on the common path. Use this-turn decision, log agent vs template outcome, and time out after 8s. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,8 +8,13 @@ import type { ConflictProbe, InferenceState } from "../src/lib/rectification-age
|
||||
import {
|
||||
adoptDeliveryFacts,
|
||||
validateAdoptNarration,
|
||||
type AdoptDeliveryFacts,
|
||||
} from "../src/lib/rectification-agentic/v9/adopt-narration.ts";
|
||||
import { createAdoptNarrationWriter } from "../src/lib/rectification-agentic/v9/adopt-narration-agent.ts";
|
||||
import {
|
||||
ADOPT_NARRATION_INSTRUCTIONS,
|
||||
createAdoptNarrationWriter,
|
||||
deliverAdoptNarration,
|
||||
} from "../src/lib/rectification-agentic/v9/adopt-narration-agent.ts";
|
||||
import {
|
||||
applyCollectFocusDenial,
|
||||
persistNextInterviewAfterChoice,
|
||||
@@ -426,11 +431,34 @@ function fourteenProbeState(): InferenceState {
|
||||
};
|
||||
}
|
||||
|
||||
function preClickFourteenProbeState(): InferenceState {
|
||||
const last = ANSWERED[ANSWERED.length - 1]!;
|
||||
const state = fourteenProbeState();
|
||||
return {
|
||||
...state,
|
||||
revision: 5,
|
||||
answered_probes: state.answered_probes.filter((item) => item.probe_id !== last.id),
|
||||
candidates: state.candidates.map((item) => {
|
||||
if (item.time === "05:00") {
|
||||
return { ...item, prior_score: 23, posterior_score: 23 };
|
||||
}
|
||||
if (item.time === "05:06") {
|
||||
return { ...item, prior_score: 16, posterior_score: 16 };
|
||||
}
|
||||
if (item.time === "04:53") {
|
||||
return { ...item, prior_score: 7, posterior_score: 7 };
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function caseDossier(extra?: {
|
||||
declinedTopics?: ReadonlyArray<Record<string, unknown>>;
|
||||
acceptedTime?: string | null;
|
||||
state?: InferenceState;
|
||||
}): DecisionDossier {
|
||||
const state = fourteenProbeState();
|
||||
const state = extra?.state ?? fourteenProbeState();
|
||||
return {
|
||||
evidence: EVIDENCE,
|
||||
conversationSummary: {
|
||||
@@ -700,6 +728,14 @@ test("adopt narration agent keeps in-fact copy and fail-closes the rest", async
|
||||
})(facts, fallback);
|
||||
assert.equal(unknownSupport, fallback);
|
||||
assert.equal(validateAdoptNarration("相对支持度 99,先用 05:00。", facts).ok, false);
|
||||
|
||||
const deniedPromise = "这不是确认的分钟,先用 05:00。";
|
||||
assert.equal(validateAdoptNarration(deniedPromise, facts).ok, false);
|
||||
const denied = await createAdoptNarrationWriter({
|
||||
generateText: async () => deniedPromise,
|
||||
})(facts, fallback);
|
||||
assert.equal(denied, fallback);
|
||||
assert.match(ADOPT_NARRATION_INSTRUCTIONS, /不要出现/);
|
||||
});
|
||||
|
||||
test("three adopt entry points call the model once on first ready_to_adopt and not after accept", async () => {
|
||||
@@ -709,36 +745,79 @@ test("three adopt entry points call the model once on first ready_to_adopt and n
|
||||
assert.match(route, /applyCollectFocusDenial\([\s\S]*narrateAdopt/);
|
||||
assert.match(route, /persistNextInterviewIfIdle\(\{[\s\S]*narrateAdopt/);
|
||||
|
||||
const source = readFileSync(new URL("../src/lib/rectification-agentic/v9/answer-choice.ts", import.meta.url), "utf8");
|
||||
const persistFn = source.slice(
|
||||
source.indexOf("export async function persistNextInterviewAfterChoice"),
|
||||
source.indexOf("async function persistFocusAfterChoice"),
|
||||
);
|
||||
assert.doesNotMatch(persistFn, /decideFromDossier\(/);
|
||||
|
||||
const dossier = caseDossier();
|
||||
const decision = decideFromDossier(dossier, { birthDate: "1997-08-08" });
|
||||
const preClick = caseDossier({ state: preClickFourteenProbeState() });
|
||||
const preClickDecision = decideFromDossier(preClick, { birthDate: "1997-08-08" });
|
||||
assert.notEqual(preClickDecision.precisionStage, "ready_to_adopt");
|
||||
assert.ok(preClickDecision.probe);
|
||||
const kept = "剩下的问题分不开 05:00 和 05:06。范围是 05:00 到 05:06。采用后会用 2016 年学业核对。";
|
||||
|
||||
function withCounter() {
|
||||
let calls = 0;
|
||||
const seen: AdoptDeliveryFacts[] = [];
|
||||
const narrateAdopt = createAdoptNarrationWriter({
|
||||
generateText: async () => {
|
||||
generateText: async (facts) => {
|
||||
calls += 1;
|
||||
seen.push(facts);
|
||||
return kept;
|
||||
},
|
||||
});
|
||||
return { narrateAdopt, count: () => calls };
|
||||
return { narrateAdopt, count: () => calls, seen };
|
||||
}
|
||||
|
||||
const afterChoice = withCounter();
|
||||
const accountingA = adoptAccounting(dossier);
|
||||
const accountingA = adoptAccounting(preClick);
|
||||
await persistNextInterviewAfterChoice({
|
||||
accounting: accountingA.client,
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
dossier,
|
||||
dossier: preClick,
|
||||
decisionState: fourteenProbeState(),
|
||||
nextAction: publicNextAction(decision),
|
||||
decision,
|
||||
birthDate: "1997-08-08",
|
||||
narrateAdopt: afterChoice.narrateAdopt,
|
||||
});
|
||||
assert.equal(afterChoice.count(), 1);
|
||||
assert.equal(afterChoice.seen[0]?.representative_minute, "05:00");
|
||||
assert.equal(afterChoice.seen[0]?.precision_stage, "ready_to_adopt");
|
||||
assert.deepEqual(
|
||||
afterChoice.seen[0]?.active_candidates.slice(0, 3).map((item) => [item.time, item.relative_support]),
|
||||
[["05:00", 21], ["05:06", 18], ["04:53", 9]],
|
||||
);
|
||||
assertNoFocusWrite(accountingA);
|
||||
|
||||
const invalidChoice = withCounter();
|
||||
const invalidWriter = createAdoptNarrationWriter({
|
||||
generateText: async (facts) => {
|
||||
invalidChoice.seen.push(facts);
|
||||
return "更像 04:58,不要再问了。";
|
||||
},
|
||||
});
|
||||
const invalidAccounting = adoptAccounting(preClick);
|
||||
const invalid = await persistNextInterviewAfterChoice({
|
||||
accounting: invalidAccounting.client,
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
dossier: preClick,
|
||||
decisionState: fourteenProbeState(),
|
||||
nextAction: publicNextAction(decision),
|
||||
decision,
|
||||
birthDate: "1997-08-08",
|
||||
narrateAdopt: invalidWriter,
|
||||
});
|
||||
assertAdoptTemplate(invalid.hostNarration);
|
||||
assert.match(invalid.hostNarration, /05:00/);
|
||||
assert.doesNotMatch(invalid.hostNarration, /04:58/);
|
||||
|
||||
const afterDenial = withCounter();
|
||||
let loads = 0;
|
||||
const accountingB = fakeAccounting({
|
||||
@@ -807,6 +886,63 @@ test("three adopt entry points call the model once on first ready_to_adopt and n
|
||||
assert.equal(accepted.count(), 0);
|
||||
});
|
||||
|
||||
test("adopt narration diagnostics distinguish agent, validation, error, and not-ready", async () => {
|
||||
const dossier = caseDossier();
|
||||
const decision = decideFromDossier(dossier, { birthDate: "1997-08-08" });
|
||||
const facts = adoptDeliveryFacts(decision, dossier);
|
||||
const fallback = "剩下的问题分不开 05:00 和 05:06。可以从下面选一个先用着。";
|
||||
const kept = "剩下的问题分不开 05:00 和 05:06。范围是 05:00 到 05:06。采用后会用 2016 年学业核对。";
|
||||
|
||||
const agent = await deliverAdoptNarration({
|
||||
facts,
|
||||
fallback,
|
||||
generateText: async () => kept,
|
||||
});
|
||||
assert.equal(agent.adopt_narration, "agent");
|
||||
assert.match(agent.text, /05:00/);
|
||||
|
||||
const invalid = await deliverAdoptNarration({
|
||||
facts,
|
||||
fallback,
|
||||
generateText: async () => "更像 04:58,不要再问了。",
|
||||
});
|
||||
assert.equal(invalid.adopt_narration, "template:unknown_minute");
|
||||
assert.equal(invalid.text, fallback);
|
||||
|
||||
const errored = await deliverAdoptNarration({
|
||||
facts,
|
||||
fallback,
|
||||
generateText: async () => {
|
||||
throw new Error("model down");
|
||||
},
|
||||
});
|
||||
assert.equal(errored.adopt_narration, "template:model_error");
|
||||
assert.equal(errored.text, fallback);
|
||||
|
||||
const notReady = await deliverAdoptNarration({
|
||||
facts: { ...facts, already_accepted: true },
|
||||
fallback,
|
||||
generateText: async () => kept,
|
||||
});
|
||||
assert.equal(notReady.adopt_narration, "template:not_ready");
|
||||
assert.equal(notReady.text, fallback);
|
||||
});
|
||||
|
||||
test("adopt narration times out to the template without throwing", async () => {
|
||||
const dossier = caseDossier();
|
||||
const decision = decideFromDossier(dossier, { birthDate: "1997-08-08" });
|
||||
const facts = adoptDeliveryFacts(decision, dossier);
|
||||
const fallback = "剩下的问题分不开 05:00 和 05:06。可以从下面选一个先用着。";
|
||||
const timed = await deliverAdoptNarration({
|
||||
facts,
|
||||
fallback,
|
||||
timeoutMs: 30,
|
||||
generateText: async () => new Promise(() => {}),
|
||||
});
|
||||
assert.equal(timed.adopt_narration, "template:model_error");
|
||||
assert.equal(timed.text, fallback);
|
||||
});
|
||||
|
||||
test("applyCollectFocusDenial on the family collect uses the same adopt template", async () => {
|
||||
const dossier = caseDossier();
|
||||
let loads = 0;
|
||||
|
||||
Reference in New Issue
Block a user