fix(rectification): unify discriminator question contract and rank by information gain
Python and TypeScript now share a four-option probe contract, persist Focus before asking, and pick the highest-value renderable probe instead of preferring low-gain career events over D24. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -300,6 +300,23 @@ test("turn_decision stays inside the configured byte budget", () => {
|
||||
assert.ok(!("baseline_birth_snapshot" in projection));
|
||||
});
|
||||
|
||||
test("turn_decision hides current_probe unless a valid current_question exists", () => {
|
||||
const withFocus = projectTurnDecision(parseV9CaseDossier(choiceDossier())!);
|
||||
assert.ok(withFocus.current_question);
|
||||
assert.ok(withFocus.current_probe);
|
||||
assert.equal((withFocus.question_contract as { version?: string }).version, "probe-question-v1");
|
||||
|
||||
const snapshot = candidateSnapshotFixture();
|
||||
Object.assign(snapshot.decision_receipt, { inference_state: inferenceState() });
|
||||
const withoutFocus = projectTurnDecision(parseV9CaseDossier(dossierFixture({
|
||||
latestResult: snapshot,
|
||||
conversationSummary: conversationSummaryFixture({ activeFocus: null }),
|
||||
}))!);
|
||||
assert.equal(withoutFocus.current_question, null);
|
||||
assert.equal(withoutFocus.current_probe, null);
|
||||
assert.equal((withoutFocus.inference as { next_probe?: unknown } | null)?.next_probe, null);
|
||||
});
|
||||
|
||||
test("truncated and timed-out runs return concrete finish reasons", () => {
|
||||
assert.equal(mapModelFinishToErrorCode({
|
||||
finishReason: "length",
|
||||
@@ -384,6 +401,12 @@ test("rectification attempt timeout stays under the agent route budget", () => {
|
||||
assert.match(regenerate, /export const maxDuration = 240/);
|
||||
assert.ok(210_000 < 240_000);
|
||||
assert.match(agentRun, /RETRYABLE_ERROR_CODES = new Set\(\[/);
|
||||
const retryable = agentRun.slice(
|
||||
agentRun.indexOf("const RETRYABLE_ERROR_CODES"),
|
||||
agentRun.indexOf("function streamFinishReason"),
|
||||
);
|
||||
assert.match(agentRun, /state_invariant_failed/);
|
||||
assert.doesNotMatch(retryable, /state_invariant_failed/);
|
||||
assert.doesNotMatch(agentRun, /stale_question/);
|
||||
assert.doesNotMatch(agentRun, /revision_conflict/);
|
||||
});
|
||||
|
||||
@@ -24,16 +24,16 @@ test("remaining D10 three-way outranks a skewed D24 split", () => {
|
||||
assert.equal(probe.choiceKind, "varga_style");
|
||||
assert.match(probe.semanticKey, /varga\.d10/);
|
||||
assert.doesNotMatch(probe.semanticKey, /varga\.d24/);
|
||||
assert.equal(probe.styleOptions?.length, 3);
|
||||
assert.equal(probe.expectedOutcomes.length, 3);
|
||||
assert.deepEqual(probe.expectedOutcomes.map((row) => row.supportsCandidateIds), [
|
||||
assert.equal(probe.styleOptions?.length, 4);
|
||||
assert.equal(probe.expectedOutcomes.length, 4);
|
||||
assert.deepEqual(probe.expectedOutcomes.filter((row) => row.outcomeId !== "unsure").map((row) => row.supportsCandidateIds), [
|
||||
["05:00"],
|
||||
["05:03"],
|
||||
["05:04"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("two-way remaining D9 maps C to unsure, not the other minute group", () => {
|
||||
test("two-way remaining D9 keeps the other minute on weak_yes, not no", () => {
|
||||
const packet = buildCandidateContrastPacket({
|
||||
candidateSetVersion: "05:00-05:04",
|
||||
candidateTimes: ["05:00", "05:04"],
|
||||
@@ -44,10 +44,11 @@ test("two-way remaining D9 maps C to unsure, not the other minute group", () =>
|
||||
const probe = selectDiscriminatorProbe(packet);
|
||||
assert.ok(probe);
|
||||
assert.equal(probe.choiceKind, "varga_style");
|
||||
assert.equal(probe.styleOptions?.length, 2);
|
||||
assert.equal(probe.styleOptions?.length, 4);
|
||||
assert.deepEqual(probe.expectedOutcomes.find((row) => row.outcomeId === "yes")?.supportsCandidateIds, ["05:00"]);
|
||||
assert.deepEqual(probe.expectedOutcomes.find((row) => row.outcomeId === "weak_yes")?.supportsCandidateIds, ["05:04"]);
|
||||
assert.equal(probe.expectedOutcomes.some((row) => row.outcomeId === "no"), false);
|
||||
assert.deepEqual(probe.expectedOutcomes.find((row) => row.outcomeId === "no")?.supportsCandidateIds, []);
|
||||
assert.equal(probe.styleOptions?.some((item) => item.answerClass === "unsure"), true);
|
||||
});
|
||||
|
||||
test("remaining D7 enters the pool as family existence", () => {
|
||||
|
||||
@@ -270,29 +270,35 @@ test("choice frame requires a real non-empty event family", () => {
|
||||
}, { probes: [{ ...MOVE_PROBE, event_family: " " }] }), null);
|
||||
});
|
||||
|
||||
test("choice frame rejects incomplete, duplicate-class, illegal, and duplicate-label style options", () => {
|
||||
const build = (style_options: DiscriminatingEventProbe["style_options"]) => buildChoiceFrame({
|
||||
test("choice frame completes existence options and rejects illegal or non-renderable varga styles", () => {
|
||||
const build = (style_options: DiscriminatingEventProbe["style_options"], extra: Partial<DiscriminatingEventProbe> = {}) => buildChoiceFrame({
|
||||
method_id: "d4_home",
|
||||
ask_theme: "home_change",
|
||||
domain: "relocation",
|
||||
user_prompt_hint: "unused",
|
||||
}, { probes: [{ ...MOVE_PROBE, style_options }] });
|
||||
}, { probes: [{ ...MOVE_PROBE, style_options, ...extra }] });
|
||||
|
||||
assert.equal(build(DYNAMIC_STYLE_OPTIONS.slice(0, 3)), null);
|
||||
assert.ok(build(DYNAMIC_STYLE_OPTIONS.slice(0, 3)));
|
||||
assert.ok(build(undefined));
|
||||
assert.equal(build([
|
||||
...DYNAMIC_STYLE_OPTIONS,
|
||||
{ label: "另一种明确发生", answer_class: "yes" },
|
||||
]), null);
|
||||
assert.equal(build([
|
||||
{ ...DYNAMIC_STYLE_OPTIONS[0], label: "外貌更接近第一种" },
|
||||
...DYNAMIC_STYLE_OPTIONS.slice(1),
|
||||
]), null);
|
||||
assert.equal(build([
|
||||
DYNAMIC_STYLE_OPTIONS[0],
|
||||
{ ...DYNAMIC_STYLE_OPTIONS[1], label: DYNAMIC_STYLE_OPTIONS[0].label },
|
||||
DYNAMIC_STYLE_OPTIONS[2],
|
||||
DYNAMIC_STYLE_OPTIONS[3],
|
||||
]), null);
|
||||
{ label: "巨蟹相处主动热情", answer_class: "yes" },
|
||||
], { choice_kind: "varga_style" }), null);
|
||||
});
|
||||
|
||||
test("python-shaped existence probe without style_options still builds a four-option card", () => {
|
||||
const { style_options: _unused, ...pythonProbe } = MOVE_PROBE;
|
||||
const frame = buildChoiceFrame({
|
||||
method_id: "d4_home",
|
||||
ask_theme: "home_change",
|
||||
domain: "relocation",
|
||||
user_prompt_hint: "unused",
|
||||
}, { probes: [pythonProbe] });
|
||||
assert.ok(frame);
|
||||
assert.equal(frame.option_a_answer_class, "yes");
|
||||
assert.equal(frame.option_b_answer_class, "weak_yes");
|
||||
assert.equal(frame.option_c_answer_class, "no");
|
||||
assert.equal(frame.option_d_answer_class, "unsure");
|
||||
assert.equal(frame.unsure_label, "这段记不清楚");
|
||||
});
|
||||
|
||||
test("style options without a real event probe do not generate a card", () => {
|
||||
@@ -902,7 +908,7 @@ test("GET choice_card stays hidden without a persisted focus after 没有了", (
|
||||
assert.equal(card, null);
|
||||
});
|
||||
|
||||
test("dynamic option labels preserve order and carry their own answer class", () => {
|
||||
test("dynamic option labels canonicalize to yes/weak_yes/no/unsure order", () => {
|
||||
const styleOptions = [
|
||||
{ label: "明确没有发生", answer_class: "no" },
|
||||
{ label: "这段记不清楚", answer_class: "unsure" },
|
||||
@@ -920,10 +926,10 @@ test("dynamic option labels preserve order and carry their own answer class", ()
|
||||
assert.ok(frame);
|
||||
const copy = serverOwnedChoiceCopy(frame);
|
||||
assert.deepEqual(copy?.options, [
|
||||
{ key: "A", ...styleOptions[0] },
|
||||
{ key: "B", ...styleOptions[1] },
|
||||
{ key: "C", ...styleOptions[2] },
|
||||
{ key: "D", ...styleOptions[3] },
|
||||
{ key: "A", label: "明确发生且时间吻合", answer_class: "yes" },
|
||||
{ key: "B", label: "发生过但程度较弱", answer_class: "weak_yes" },
|
||||
{ key: "C", label: "明确没有发生", answer_class: "no" },
|
||||
{ key: "D", label: "这段记不清楚", answer_class: "unsure" },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1255,6 +1255,10 @@ test("public tool surface stays at 14 and new cases bind 10.0.13", () => {
|
||||
assert.match(skill, /D9\/D10 类型表是校时方法/);
|
||||
assert.doesNotMatch(skill, /±5 分钟确定性/);
|
||||
assert.doesNotMatch(skill, /KP 政策跳过不挡提出门/);
|
||||
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
|
||||
assert.match(tools, /function agentVisibleLatestProjection/);
|
||||
assert.match(tools, /candidate_contrast_packet: _packet/);
|
||||
assert.match(tools, /current_probe: null/);
|
||||
});
|
||||
|
||||
test("Mastra hides active candidates when the receipt range excludes one of them", () => {
|
||||
@@ -1780,6 +1784,47 @@ test("answered duty language skips window D10 and uses remaining D24", () => {
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d10/);
|
||||
});
|
||||
|
||||
test("low-gain career event probe does not outrank a renderable high-gain D24 contrast probe", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
|
||||
eventProbes: [{
|
||||
...CAREER_CONFLICT_PROBE,
|
||||
year: 2023,
|
||||
year_label: "2023 年前后",
|
||||
semantic_key: "career.2023.dasha_activation",
|
||||
information_gain: 0.56,
|
||||
candidate_split_hash: "set-test:career:2023",
|
||||
}],
|
||||
contrastPacket: {
|
||||
candidateSetVersion: "05:00-05:14",
|
||||
vargaDifferences: [],
|
||||
probes: [{
|
||||
probeId: "contrast:varga.d24.05:00/05:07|05:10|05:14",
|
||||
candidateSetVersion: "05:00-05:14",
|
||||
question: "当前几个候选在学业盘上还分得开。",
|
||||
expectedOutcomes: [
|
||||
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:07", "05:10", "05:14"] },
|
||||
{ outcomeId: "no", supportsCandidateIds: ["05:07", "05:10", "05:14"], conflictsCandidateIds: ["05:00"] },
|
||||
],
|
||||
candidateSplitHash: "varga.d24.05:00/05:07|05:10|05:14",
|
||||
informationGain: 2.503258,
|
||||
sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }],
|
||||
domain: "education",
|
||||
year: null,
|
||||
semanticKey: "varga.d24.05:00/05:07|05:10|05:14",
|
||||
choiceKind: "event_quality",
|
||||
}],
|
||||
},
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.equal(plan.next_followup?.semantic_key, "varga.d24.05:00/05:07|05:10|05:14");
|
||||
assert.equal(plan.next_followup?.choice_frame?.option_d_answer_class, "unsure");
|
||||
assert.equal(plan.next_followup?.choice_frame?.period, "当前这几个候选");
|
||||
assert.match(plan.next_followup?.choice_frame?.prompt ?? "", /学业|考试发挥|学习压力/);
|
||||
assert.doesNotMatch(plan.next_followup?.choice_frame?.prompt ?? "", /入职、升职/);
|
||||
assert.ok((plan.next_followup?.selection_score ?? 0) > 0.56);
|
||||
});
|
||||
|
||||
const DUMP_COVERAGE = [
|
||||
{
|
||||
status: "confirmed" as const,
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
EXISTENCE_STYLE_OPTIONS,
|
||||
QUALITY_STYLE_OPTIONS,
|
||||
QUESTION_CONTRACT_VERSION,
|
||||
completeStyleOptions,
|
||||
isRenderableProbe,
|
||||
rankDiscriminatorScore,
|
||||
} from "../src/lib/rectification-agentic/v9/probe-question-contract.ts";
|
||||
|
||||
test("existence probes complete to four answer classes without engine style_options", () => {
|
||||
const completed = completeStyleOptions({ choiceKind: "existence" });
|
||||
assert.deepEqual(completed, [...EXISTENCE_STYLE_OPTIONS]);
|
||||
assert.equal(QUESTION_CONTRACT_VERSION, "probe-question-v1");
|
||||
});
|
||||
|
||||
test("event_quality probes use quality labels and still cover unsure", () => {
|
||||
const completed = completeStyleOptions({ choiceKind: "event_quality" });
|
||||
assert.deepEqual(completed, [...QUALITY_STYLE_OPTIONS]);
|
||||
assert.equal(completed?.some((item) => item.answer_class === "unsure"), true);
|
||||
});
|
||||
|
||||
test("varga-style probes stay dynamic and fail closed without two scoring labels", () => {
|
||||
assert.equal(completeStyleOptions({
|
||||
choiceKind: "varga_style",
|
||||
styleOptions: [{ label: "巨蟹相处主动热情", answer_class: "yes" }],
|
||||
}), null);
|
||||
const completed = completeStyleOptions({
|
||||
choiceKind: "varga_style",
|
||||
styleOptions: [
|
||||
{ label: "巨蟹相处主动热情", answer_class: "yes", sign: "巨蟹" },
|
||||
{ label: "狮子独立强势", answer_class: "weak_yes", sign: "狮子" },
|
||||
],
|
||||
});
|
||||
assert.equal(completed?.length, 4);
|
||||
assert.equal(completed?.find((item) => item.answer_class === "no")?.label, "都不是这些特质");
|
||||
assert.equal(completed?.find((item) => item.answer_class === "unsure")?.label, "这段记不清楚");
|
||||
});
|
||||
|
||||
test("illegal clock or appearance copy cannot become a renderable probe", () => {
|
||||
assert.equal(isRenderableProbe({
|
||||
informationGain: 1.2,
|
||||
candidateIds: ["05:00", "05:04"],
|
||||
expectedOutcomeCount: 2,
|
||||
choiceKind: "existence",
|
||||
styleOptions: [
|
||||
{ label: "08:12 左右发生", answer_class: "yes" },
|
||||
],
|
||||
}), true);
|
||||
assert.equal(completeStyleOptions({
|
||||
choiceKind: "existence",
|
||||
styleOptions: [{ label: "08:12 左右发生", answer_class: "yes" }],
|
||||
})?.find((item) => item.answer_class === "yes")?.label, "明确发生且时间吻合");
|
||||
assert.equal(isRenderableProbe({
|
||||
informationGain: 1.2,
|
||||
candidateIds: ["05:00", "05:04"],
|
||||
expectedOutcomeCount: 2,
|
||||
choiceKind: "varga_style",
|
||||
styleOptions: [
|
||||
{ label: "外貌更接近第一种", answer_class: "yes" },
|
||||
{ label: "相处更独立", answer_class: "weak_yes" },
|
||||
],
|
||||
}), false);
|
||||
});
|
||||
|
||||
test("asked probes keep a novelty penalty so unused high-gain probes rank first", () => {
|
||||
const askedCareer = rankDiscriminatorScore({
|
||||
informationGain: 0.56,
|
||||
asked: true,
|
||||
candidateIds: ["05:00", "05:07"],
|
||||
});
|
||||
const unusedD24 = rankDiscriminatorScore({
|
||||
informationGain: 2.5,
|
||||
asked: false,
|
||||
candidateIds: ["05:00", "05:07", "05:10"],
|
||||
});
|
||||
assert.ok(unusedD24 > askedCareer);
|
||||
assert.ok(unusedD24 > 0.56);
|
||||
});
|
||||
@@ -69,15 +69,18 @@ function discriminatorFollowup(overrides: Partial<MethodFollowup> = {}): MethodF
|
||||
function invalidStyleFollowup(
|
||||
styleOptions: readonly EventProbeStyleOption[] | undefined,
|
||||
eventFamily = "升学结果或学习环境出现明显变化",
|
||||
choiceKind: "existence" | "varga_style" | "event_quality" = "existence",
|
||||
): MethodFollowup {
|
||||
const valid = discriminatorFollowup();
|
||||
return {
|
||||
...valid,
|
||||
choice_kind: choiceKind,
|
||||
choice_frame: buildChoiceFrame({
|
||||
method_id: "dasha_events",
|
||||
ask_theme: "dated_event",
|
||||
domain: "education",
|
||||
user_prompt_hint: "ask",
|
||||
choice_kind: choiceKind,
|
||||
}, {
|
||||
probes: [{
|
||||
year: 2016,
|
||||
@@ -92,6 +95,7 @@ function invalidStyleFollowup(
|
||||
role: "distinguish",
|
||||
information_gain: 0.4,
|
||||
semantic_key: "education:2016",
|
||||
choice_kind: choiceKind,
|
||||
style_options: styleOptions,
|
||||
}],
|
||||
}),
|
||||
@@ -219,17 +223,12 @@ test("complete dynamic event options persist a server-owned focus", async () =>
|
||||
});
|
||||
});
|
||||
|
||||
test("missing, duplicate, incomplete, or illegal dynamic options skip focus persistence", async () => {
|
||||
await assertInvalidChoiceSkipped(invalidStyleFollowup(undefined));
|
||||
await assertInvalidChoiceSkipped(invalidStyleFollowup(DYNAMIC_STYLE_OPTIONS.slice(0, 3)));
|
||||
await assertInvalidChoiceSkipped(invalidStyleFollowup([
|
||||
...DYNAMIC_STYLE_OPTIONS,
|
||||
{ label: "另一种明确发生", answer_class: "yes" },
|
||||
]));
|
||||
await assertInvalidChoiceSkipped(invalidStyleFollowup([
|
||||
{ ...DYNAMIC_STYLE_OPTIONS[0], label: "08:12 左右发生" },
|
||||
...DYNAMIC_STYLE_OPTIONS.slice(1),
|
||||
]));
|
||||
test("non-renderable varga styles and empty event family skip focus persistence", async () => {
|
||||
await assertInvalidChoiceSkipped(invalidStyleFollowup(
|
||||
[{ label: "巨蟹相处主动热情", answer_class: "yes" }],
|
||||
"升学结果或学习环境出现明显变化",
|
||||
"varga_style",
|
||||
));
|
||||
await assertInvalidChoiceSkipped(invalidStyleFollowup(DYNAMIC_STYLE_OPTIONS, " "));
|
||||
});
|
||||
|
||||
|
||||
@@ -103,5 +103,8 @@ test("production intent handling contains no semantic regex or positional text p
|
||||
assert.doesNotMatch(classifier + fastPath, /\.test\([^\n]*(?:userMessage|user_message|message)/);
|
||||
assert.doesNotMatch(classifier + fastPath, /(?:userMessage|user_message|message)\.(?:match|search|includes|startsWith|endsWith)\(/);
|
||||
assert.ok(route.indexOf("classifyRectificationTurnIntent") < route.indexOf("runV9AgentTurn({"));
|
||||
assert.ok(route.indexOf("persistServerOwnedFocus") < route.indexOf("runV9AgentTurn({"));
|
||||
assert.ok(fastPath.includes("ask_candidate_discriminator"));
|
||||
assert.match(fastPath, /目前没有可继续区分/);
|
||||
assert.doesNotMatch(route, /classified\.answer_class!/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user