fix(web): hold reverse-inference cards until acceptance event quality
Conflict probes were jumping after one dated event, so the interview asked another domain before method collection. Spoken replies now follow the stamped choice prompt instead of a topic denylist. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -243,6 +243,8 @@ test("usage completes or releases without hiding settlement failures", () => {
|
||||
assert.match(run, /thinkingTokens: 8_192/);
|
||||
assert.match(run, /activity.changed/);
|
||||
assert.match(run, /if \(!answerText\.trim\(\)\) \{[\s\S]*composeRectificationTurnNarration/);
|
||||
assert.match(run, /bindSpokenToOpenQuestion/);
|
||||
assert.match(run, /openQuestionPromptFromToolResult/);
|
||||
assert.doesNotMatch(run, /splitRectificationSpokenAndThinking/);
|
||||
assert.match(run, /emit\(event: PublicStreamEvent\): Promise<void> \| void;/);
|
||||
assert.match(run, /agentGenerationSettings\(options\.generationModel, \{[\s\S]*thinking: "enabled"/);
|
||||
@@ -637,6 +639,8 @@ test("the Agent prompt cannot offer candidates while asking for more evidence",
|
||||
assert.match(agent, /id 不是 adopt_representative 时不得调用 rectification-offer-candidates/);
|
||||
assert.match(agent, /verify_adopted_time/);
|
||||
assert.match(agent, /event_probe/);
|
||||
assert.match(agent, /至少 3 件/);
|
||||
assert.match(agent, /2 个领域/);
|
||||
assert.match(agent, /selection_allowed 只表示可以采用代表性时间/);
|
||||
const tools = readFileSync(
|
||||
new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url),
|
||||
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
|
||||
const THIRD_CANDIDATE_ID = "88888888-8888-4888-8888-888888888883";
|
||||
const EDUCATION_ID = "44444444-4444-4444-8444-444444444441";
|
||||
const RELATIONSHIP_ID = "44444444-4444-4444-8444-444444444442";
|
||||
const FAMILY_ID = "44444444-4444-4444-8444-444444444443";
|
||||
const UNIQUE_MINUTE_COPY = /±5 分钟确定性/;
|
||||
|
||||
const CLASSIC_COVERAGE = [
|
||||
@@ -50,6 +52,39 @@ const CLASSIC_COVERAGE = [
|
||||
{ status: "confirmed", domain: "horary", datePrecision: "day" as const, occurredFrom: "2024-01-01", occurredTo: null },
|
||||
];
|
||||
|
||||
function datedEvidence(
|
||||
domain: string,
|
||||
year: string,
|
||||
extra: {
|
||||
eventKind?: string | null;
|
||||
summary?: string | null;
|
||||
datePrecision?: "year" | "month" | "day";
|
||||
} = {},
|
||||
) {
|
||||
return {
|
||||
status: "confirmed" as const,
|
||||
domain,
|
||||
datePrecision: extra.datePrecision ?? ("year" as const),
|
||||
occurredFrom: `${year}-01-01`,
|
||||
occurredTo: null,
|
||||
...(extra.eventKind !== undefined ? { eventKind: extra.eventKind } : {}),
|
||||
...(extra.summary !== undefined ? { summary: extra.summary } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const CAREER_CONFLICT_PROBE = {
|
||||
year: 2018,
|
||||
year_label: "2018 年前后",
|
||||
domain: "career" as const,
|
||||
event_family: "入职、升职或职责明显加重",
|
||||
source: "dasha_activation" as const,
|
||||
tracks: ["vimshottari", "narayana"] as const,
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false as const,
|
||||
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
|
||||
role: "reverse_verify" as const,
|
||||
};
|
||||
|
||||
const ENGINE_SCORE = {
|
||||
success: true,
|
||||
endpoint: "rectification_v5_score",
|
||||
@@ -113,6 +148,36 @@ const educationEvidence = {
|
||||
created_at: "2026-08-12T10:00:06.000Z",
|
||||
};
|
||||
|
||||
const relationshipEvidence = {
|
||||
id: RELATIONSHIP_ID,
|
||||
source_turn_id: TURN_ID,
|
||||
subject: "self",
|
||||
event_kind: "relationship_start",
|
||||
domain: "relationship",
|
||||
occurred_from: "2018-01-01",
|
||||
occurred_to: null,
|
||||
date_precision: "year",
|
||||
summary: "2018年一段感情开始",
|
||||
status: "confirmed",
|
||||
supersedes_evidence_id: null,
|
||||
created_at: "2026-08-12T10:00:07.000Z",
|
||||
};
|
||||
|
||||
const familyEvidence = {
|
||||
id: FAMILY_ID,
|
||||
source_turn_id: TURN_ID,
|
||||
subject: "family",
|
||||
event_kind: "family_event",
|
||||
domain: "family",
|
||||
occurred_from: "2020-01-01",
|
||||
occurred_to: null,
|
||||
date_precision: "year",
|
||||
summary: "2020年家人相关变化",
|
||||
status: "confirmed",
|
||||
supersedes_evidence_id: null,
|
||||
created_at: "2026-08-12T10:00:08.000Z",
|
||||
};
|
||||
|
||||
function stubEngine(response: unknown) {
|
||||
const previous = globalThis.fetch;
|
||||
globalThis.fetch = (async () => ({
|
||||
@@ -142,27 +207,25 @@ test("eight-method routing asks relationship after dated education, not relocati
|
||||
assert.equal(plan.methods.find((item) => item.method_id === "appearance")?.status, "skipped_by_policy");
|
||||
});
|
||||
|
||||
test("dated evidence plus dasha conflict probe asks that event before method rotation and blocks offer", () => {
|
||||
test("dasha conflict probe does not jump ahead of method rotation before acceptance event quality", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [{
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2016-01-01",
|
||||
occurredTo: null,
|
||||
}],
|
||||
eventProbes: [{
|
||||
year: 2018,
|
||||
year_label: "2018 年前后",
|
||||
domain: "career",
|
||||
event_family: "入职、升职或职责明显加重",
|
||||
source: "dasha_activation",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
|
||||
role: "reverse_verify",
|
||||
}],
|
||||
evidence: [datedEvidence("education", "2016")],
|
||||
eventProbes: [CAREER_CONFLICT_PROBE],
|
||||
});
|
||||
assert.equal(plan.next_followup?.source, "method_coverage");
|
||||
assert.equal(plan.next_followup?.method_id, "d9_relationship");
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
assert.notEqual(plan.next_followup?.source, "event_probe");
|
||||
});
|
||||
|
||||
test("dasha conflict probe jumps after three scoreable events in two domains and blocks offer", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
datedEvidence("education", "2016"),
|
||||
datedEvidence("education", "2020"),
|
||||
datedEvidence("relationship", "2018"),
|
||||
],
|
||||
eventProbes: [CAREER_CONFLICT_PROBE],
|
||||
});
|
||||
assert.equal(plan.next_followup?.source, "event_probe");
|
||||
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
|
||||
@@ -181,6 +244,41 @@ test("dated evidence plus dasha conflict probe asks that event before method rot
|
||||
}), "collect_evidence");
|
||||
});
|
||||
|
||||
test("three scoreable events in one domain still rotate methods instead of reverse-inferring", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
datedEvidence("education", "2012"),
|
||||
datedEvidence("education", "2016"),
|
||||
datedEvidence("education", "2020"),
|
||||
],
|
||||
eventProbes: [CAREER_CONFLICT_PROBE],
|
||||
});
|
||||
assert.equal(plan.next_followup?.source, "method_coverage");
|
||||
assert.equal(plan.next_followup?.method_id, "d9_relationship");
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
});
|
||||
|
||||
test("occupation_note does not count toward reverse-inference event quality", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
datedEvidence("education", "2016"),
|
||||
datedEvidence("education", "2020"),
|
||||
{
|
||||
status: "confirmed",
|
||||
domain: "occupation",
|
||||
datePrecision: "unknown",
|
||||
occurredFrom: null,
|
||||
occurredTo: null,
|
||||
eventKind: "occupation_note",
|
||||
},
|
||||
],
|
||||
eventProbes: [CAREER_CONFLICT_PROBE],
|
||||
});
|
||||
assert.equal(plan.next_followup?.source, "method_coverage");
|
||||
assert.equal(plan.next_followup?.method_id, "d9_relationship");
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
});
|
||||
|
||||
test("age-band probe does not jump ahead of uncovered relationship", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [{
|
||||
@@ -689,7 +787,8 @@ test("evidence batch returns the persisted choice prompt as open_question", asyn
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture({
|
||||
evidence: [educationEvidence],
|
||||
evidence: [educationEvidence, relationshipEvidence, familyEvidence],
|
||||
evidenceCount: 3,
|
||||
latestResult: null,
|
||||
}),
|
||||
get_agentic_rectification_case_compute: () => computeFixture(),
|
||||
@@ -1500,24 +1599,13 @@ test("没有了 is a user stop", () => {
|
||||
|
||||
test("same domain different year still asks a conflict probe", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [{
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2015-01-01",
|
||||
occurredTo: null,
|
||||
}],
|
||||
evidence: [
|
||||
datedEvidence("education", "2016"),
|
||||
datedEvidence("relationship", "2018"),
|
||||
datedEvidence("career", "2015"),
|
||||
],
|
||||
eventProbes: [{
|
||||
year: 2018,
|
||||
year_label: "2018 年前后",
|
||||
domain: "career",
|
||||
event_family: "入职、升职或职责明显加重",
|
||||
source: "dasha_activation",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
|
||||
role: "reverse_verify",
|
||||
...CAREER_CONFLICT_PROBE,
|
||||
information_gain: 0.21,
|
||||
semantic_key: "career.2018.dasha_activation",
|
||||
}],
|
||||
|
||||
@@ -79,14 +79,17 @@ test("system prompt carries only high-priority boundaries, never the method copy
|
||||
assert.match(prompt, /不要调用 rectification-set-focus/);
|
||||
assert.match(prompt, /open_question\.prompt/);
|
||||
assert.match(prompt, /current_question\.prompt/);
|
||||
assert.match(prompt, /不得另起高考发挥/);
|
||||
assert.match(prompt, /不要另写追问/);
|
||||
assert.match(prompt, /运行器会把口语接到这句题干/);
|
||||
assert.doesNotMatch(prompt, /不得另起高考发挥/);
|
||||
assert.doesNotMatch(prompt, /不得根据出生年推算高考或入学年份/);
|
||||
assert.doesNotMatch(prompt, /不要再问那一件发生在哪一年/);
|
||||
assert.match(prompt, /「先这样」由服务器/);
|
||||
assert.match(prompt, /盘外核对(不计分)/);
|
||||
assert.match(prompt, /verify_adopted_time/);
|
||||
assert.match(prompt, /event_probe/);
|
||||
assert.match(prompt, /不得发明年份/);
|
||||
assert.match(prompt, /不得根据出生年推算高考或入学年份/);
|
||||
assert.match(prompt, /不要再问那一件发生在哪一年/);
|
||||
assert.match(prompt, /至少 3 件/);
|
||||
assert.match(prompt, /2 个领域/);
|
||||
assert.doesNotMatch(prompt, /两套盘各自的前事/);
|
||||
assert.doesNotMatch(prompt, /外貌、体质、胎记或疤痕可以问/);
|
||||
assert.doesNotMatch(prompt, /分盘句和宫位表由界面展示/);
|
||||
|
||||
@@ -23,6 +23,10 @@ import {
|
||||
import { RECTIFICATION_SKILL_NAME } from "../src/lib/rectification-agentic/v9/case-status.ts";
|
||||
import { messageContentHash } from "../src/lib/rectification-agentic/v9/message-origin.ts";
|
||||
import { safeToolErrorCode } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import {
|
||||
bindSpokenToOpenQuestion,
|
||||
openQuestionPromptFromToolResult,
|
||||
} from "../src/lib/rectification-agentic/v9/turn-narration.ts";
|
||||
import {
|
||||
createRectificationActivityReceiptState,
|
||||
receiptFromRectificationActivityState,
|
||||
@@ -1358,6 +1362,44 @@ test("Chinese interview planning stays on reasoning-delta; the spoken answer is
|
||||
assert.equal(emitted.some((event) => event.type === "thinking.delta"), false);
|
||||
});
|
||||
|
||||
test("persisted choice prompt replaces a competing model follow-up without a topic denylist", async () => {
|
||||
const spoken = "好的,2020 年 6 月毕业这条也记下了。\n\n再问你一件:2016 年前后那场重要的入学考试,你当时发挥明显失常、或者压力特别大,有没有发生过?";
|
||||
const prompt = "2023 年前后,有没有明显入职、升职或职责明显加重?";
|
||||
assert.equal(
|
||||
bindSpokenToOpenQuestion(spoken, prompt),
|
||||
`好的,2020 年 6 月毕业这条也记下了。\n\n${prompt}`,
|
||||
);
|
||||
assert.equal(openQuestionPromptFromToolResult({
|
||||
type: "tool-result",
|
||||
payload: { result: { open_question: { prompt } } },
|
||||
}), prompt);
|
||||
|
||||
const { options, emitted } = runOptions({
|
||||
buildAgent: async () => fakeAgentStream([
|
||||
chunk("start"),
|
||||
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
|
||||
chunk("tool-result", { toolName: "skill" }),
|
||||
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
|
||||
chunk("tool-result", { toolName: "rectification-read-case" }),
|
||||
chunk("tool-call", { toolName: "rectification-record-evidence-batch", args: { caseId: CASE_ID } }),
|
||||
chunk("tool-result", {
|
||||
toolName: "rectification-record-evidence-batch",
|
||||
result: { accepted_count: 1, open_question: { prompt } },
|
||||
}),
|
||||
chunk("text-delta", { text: spoken }),
|
||||
chunk("finish"),
|
||||
]) as never,
|
||||
});
|
||||
const result = await runV9AgentTurn(options);
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.answerText, `好的,2020 年 6 月毕业这条也记下了。\n\n${prompt}`);
|
||||
assert.doesNotMatch(result.answerText, /入学考试/);
|
||||
assert.deepEqual(
|
||||
emitted.filter((event) => event.type === "answer.delta"),
|
||||
[{ type: "answer.delta", text: result.answerText }],
|
||||
);
|
||||
});
|
||||
|
||||
test("model terminal text-delta is the reply even when Case narration could be composed", async () => {
|
||||
const spoken = "职业已经记下。你入职大概是哪一年?说个年份就行。";
|
||||
const { options, emitted } = runOptions({
|
||||
|
||||
Reference in New Issue
Block a user