Review-only snapshot for BUG-1115 through BUG-1117; not merge-ready. New opening and append-turn PostgreSQL permission failures remain blocked. Persisted joint replay has zero completed questions; segment ordering remains off by default. The existing offline replay JSON is retained stale and unchanged after a denied overwrite, including its CRLF line endings. Browser/provider validation and final serial gates remain pending. No deployment, role permission changes, or staging/main push. Co-Authored-By: Claude Code <noreply@anthropic.com>
87 lines
5.4 KiB
TypeScript
87 lines
5.4 KiB
TypeScript
/**
|
||
* The two messages a rectification agent attempt sends the model: the bound
|
||
* Skill bootstrap (with the retry constraint on a second attempt) and the user
|
||
* turn (the opening brief, or the typed message), both behind the server time
|
||
* and Case id lines. Moved out of agent-run.ts unchanged
|
||
* (TASK-rectification-code-split-20260926); agent-run.ts re-exports both
|
||
* builders.
|
||
*/
|
||
import type { V9CaseDossier } from "./tool-service";
|
||
import { cachedSystemMessage } from "../../agent-generation-settings.ts";
|
||
import { OPENING_COLLECT_DOMAINS } from "../user-copy";
|
||
import { retryConstraintForAttempt } from "./host-fallback";
|
||
import { sliceRectificationSkill } from "./skill-slice";
|
||
import type { V9AgentRunOptions } from "./agent-run";
|
||
import { productDeliveryForDomain } from "./product-delivery.ts";
|
||
|
||
function clockWindow(range: { start_time?: string | null; end_time?: string | null } | null | undefined): string | null {
|
||
const start = range?.start_time?.trim().slice(0, 5) || "";
|
||
const end = range?.end_time?.trim().slice(0, 5) || "";
|
||
return start && end ? `${start}–${end}` : null;
|
||
}
|
||
|
||
export function buildOpeningBrief(dossier: V9CaseDossier, birthTimeClue?: string | null): string {
|
||
const confirmed = dossier.evidence.filter((item) => item.status === "confirmed");
|
||
const pending = dossier.evidence.filter((item) => item.status === "draft" || item.status === "pending_confirmation");
|
||
const domains = [...new Set(confirmed.map((item) => item.domain))].slice(0, 6);
|
||
const range = dossier.case.candidateRange;
|
||
const window = clockWindow(range);
|
||
const productDelivery = productDeliveryForDomain(dossier.case.rectificationDomain);
|
||
const openingGoal = productDelivery
|
||
? `我们先判断现在的出生时间能不能选定用来解读的星盘${window ? `,核对窗口是 ${window}` : ""}。`
|
||
: `我们来把你的出生时间缩小到更准的范围${window ? `,现在先在 ${window} 之间找` : ""}。`;
|
||
const uncertaintyType = window
|
||
? `当前搜索窗口 ${window},来自用户在资料里声明的不确定档`
|
||
: "用户的出生时间精度仍需通过经历证据核对";
|
||
const clue = typeof birthTimeClue === "string" && birthTimeClue.trim()
|
||
? birthTimeClue.trim()
|
||
: "";
|
||
return [
|
||
"【服务端 opening brief】",
|
||
`Case 状态:${dossier.case.status}。`,
|
||
`当前搜索窗口:${window ?? "尚未锁定"}。来源:intake 声明的不确定档。`,
|
||
`出生时间不确定类型:${uncertaintyType}。`,
|
||
`已有证据摘要:已确认 ${confirmed.length} 条,待澄清或待确认 ${pending.length} 条${domains.length ? `;已覆盖 ${domains.join("、")}` : ""}。`,
|
||
...(clue
|
||
? [`家人或本人关于出生时段的线索(仅旁白建议,不得改搜索窗口):${clue}`]
|
||
: []),
|
||
`做法要点:正文两句大白话,照这个意思写:「${openingGoal}」「做法很简单:你说几件人生里的大事和大概年月,我拿去和星盘对照。」正文不用大运、盘面、分盘、候选、区间、代表分钟、精确到秒这类词,不写具体年份,不要提问,不列举例子,不要求先准备材料。一条消息可以报多件,想到几件说几件。先用 rectification-set-focus 的 spokenPrompt 写出当前采集题,写「先说一两件你记得的大事」即可:开场题干由服务端固定,会列出${OPENING_COLLECT_DOMAINS.join("、")},并给一个带大概年月的回答示例,正文不要重复这些例子。`,
|
||
].join("\n");
|
||
}
|
||
|
||
export function buildAgentMessages(
|
||
options: V9AgentRunOptions,
|
||
attempt: number,
|
||
dossier: V9CaseDossier,
|
||
skillInstructions: string,
|
||
birthTimeClue: string | null = null,
|
||
previousErrorCode: string | null = null,
|
||
): unknown[] {
|
||
const timeContext = options.timeContext
|
||
?? `服务端当前时间(权威):${new Date().toISOString()}。涉及“现在、今天、今年、未来几个月”等相对时间时,以此为准。`;
|
||
const caseContext = `【服务端 Case ID】${options.caseId}。所有 rectification 工具调用的 caseId 必须原样使用此值。`;
|
||
// T6 (TASK-rectification-grounding-20260927): an evidence turn gets only
|
||
// the Skill sections it uses; other actions keep the whole bound body.
|
||
const boundSkill = sliceRectificationSkill(skillInstructions, options.action).text;
|
||
const bootstrapContent = [
|
||
"【服务器已绑定当前 Case 的精确 Skill】运行器已在本 attempt 内加载并核验下列指令;不要重复调用 skill。第一步必须调用 rectification-read-case。",
|
||
boundSkill,
|
||
...(productDeliveryForDomain(dossier.case.rectificationDomain) ? [
|
||
`【服务端持久产品身份】${JSON.stringify(productDeliveryForDomain(dossier.case.rectificationDomain))}。本轮按目标盘型交付,不能因摘要缺失退分钟优先;盘型事实只读工具返回的segment_summary,代表签名仅诊断,不能作实际采用盘。`,
|
||
] : []),
|
||
...(attempt > 1 ? [retryConstraintForAttempt(previousErrorCode)] : []),
|
||
].join("\n\n");
|
||
const bootstrap = cachedSystemMessage(bootstrapContent, options.generationModel)
|
||
?? { role: "system" as const, content: bootstrapContent };
|
||
if (options.action === "opening") {
|
||
return [bootstrap, {
|
||
role: "user",
|
||
content: [timeContext, caseContext, buildOpeningBrief(dossier, birthTimeClue)].join("\n"),
|
||
}];
|
||
}
|
||
return [bootstrap, {
|
||
role: "user",
|
||
content: [timeContext, caseContext, options.message ?? ""].join("\n"),
|
||
}];
|
||
}
|