/** * V9 rectification tools (Case-ref only). * * The model may only pass minimal references: caseId, quote, proposedKind, * evidenceId, resultId, candidateId (plus optional date fields * the model grounded in the user's own words). userId, birth data, candidate * ranges, event arrays, scores and permission booleans never appear in tool * inputs. Every tool authenticates through the Case row, validates ownership * / status / skill version, reads the PostgreSQL evidence ledger, persists a * tool receipt and returns a compact safe projection. */ import { createTool } from "@mastra/core/tools"; import { z } from "zod"; import type { SupabaseClient } from "@supabase/supabase-js"; import type { PublicRectificationMethod } from "@/lib/rectification-agentic/v9/public-receipt"; import { canonicalToolInputFingerprint, candidateRangeFingerprint, evidenceLedgerFingerprint, insertV9ToolReceipt, loadV9CaseCompute, loadV9CaseDossier, proposeV9Evidence, confirmV9Evidence, reviseV9Evidence, transitionV9CaseStatus, persistV9Candidate, acceptV9Candidate, confirmV9BirthTime, closeV9Case, safeToolErrorCode, scorableEvidence, RectificationToolServiceError, } from "@/lib/rectification-agentic/v9/tool-service"; import { isEvidenceKind, isEvidenceDomain, isDatePrecision } from "@/lib/rectification-agentic/v9/evidence-model"; import { isResumableStatus, RECTIFICATION_SKILL_NAME, RECTIFICATION_SKILL_VERSION, type RectificationCaseStatus, } from "@/lib/rectification-agentic/v9/case-status"; import { runV9CandidateScore, runV9Diagnostics, toEngineEvents, v9EngineVersion, type V9EngineScoreResult, } from "@/lib/rectification-agentic/v9/engine-client"; const timePattern = /^(?:[01]\d|2[0-3]):[0-5]\d$/; const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; export type RectificationV9Context = Readonly<{ userId: string; caseId: string; turnId: string; accounting: SupabaseClient; engineBase?: string; }>; const dateLike = /^\d{4}(?:-\d{1,2}(?:-\d{1,2})?)?$/; function safeBirthContext(compute: Awaited>): Record { const snapshot = compute.baselineBirthSnapshot; return { birth_date: String(snapshot.birth_date ?? ""), reported_birth_time: typeof snapshot.reported_birth_time === "string" ? snapshot.reported_birth_time : null, active_birth_time: typeof snapshot.active_birth_time === "string" ? snapshot.active_birth_time : null, candidate_range: compute.candidateRange, birthplace: { label: String(snapshot.birth_place_label ?? ""), latitude: Number(snapshot.latitude), longitude: Number(snapshot.longitude), }, timezone: { id: String(snapshot.timezone_id ?? ""), offset: Number(snapshot.timezone_offset), }, }; } function safeCaseProjection( dossier: ReturnType, compute: Awaited>, ): Record { const caseRow = dossier.case; const latest = dossier.latestResult; return { case_id: caseRow.caseId, status: caseRow.status, skill_name: caseRow.skillName, skill_version: caseRow.skillVersion, candidate_range: caseRow.candidateRange, accepted_time: caseRow.acceptedTime, confirmed_time: caseRow.confirmedTime, completed_at: caseRow.completedAt, closed_reason: caseRow.closedReason, evidence_count: caseRow.evidenceCount, turn_count: caseRow.turnCount, evidence_summary: { confirmed: dossier.confirmedEvidence.length, pending: dossier.pendingEvidence.length, by_domain: dossier.domainCounts, by_kind: dossier.kindCounts, scorable: dossier.scorable.length, }, birth_context: safeBirthContext(compute), latest_result: latest ? { result_id: latest.resultId, candidates: latest.candidates, selection_allowed: latest.selectionAllowed, confirmation_allowed: latest.confirmationAllowed, representative_time: latest.representativeTime, selected_time: latest.selectedTime, selection_kind: latest.selectionKind, algorithm_version: latest.algorithmVersion, } : null, }; } type DossierForTools = { case: { caseId: string; sessionId: string; status: RectificationCaseStatus | string; skillName: string; skillVersion: string; candidateRange: { start_time: string; end_time: string } | null; acceptedTime: string | null; confirmedTime: string | null; completedAt: string | null; closedReason: string | null; evidenceCount: number; turnCount: number; }; evidence: ReturnType; confirmedEvidence: readonly unknown[]; pendingEvidence: readonly unknown[]; scorable: readonly unknown[]; domainCounts: Record; kindCounts: Record; latestResult: { resultId: string; candidates: readonly { rank: number; time: string; relative_support: number; tied_minute_count: number }[]; selectionAllowed: boolean; confirmationAllowed: boolean; representativeTime: string | null; selectedTime: string | null; selectionKind: string | null; algorithmVersion: string | null; } | null; turns: readonly { id: string; role: string; text: string | null }[]; }; function parseDossierForTools(dossier: import("@/lib/rectification-agentic/v9/tool-service").V9CaseDossier): DossierForTools { const confirmedEvidence = dossier.evidence.filter((item) => item.status === "confirmed"); const pendingEvidence = dossier.evidence.filter((item) => item.status === "pending_confirmation" || item.status === "draft"); const scorable = scorableEvidence(dossier.evidence); const domainCounts: Record = {}; const kindCounts: Record = {}; for (const item of scorable) { domainCounts[item.domain] = (domainCounts[item.domain] ?? 0) + 1; kindCounts[item.eventKind] = (kindCounts[item.eventKind] ?? 0) + 1; } return { case: { caseId: dossier.case.caseId, sessionId: dossier.case.sessionId, status: dossier.case.status, skillName: dossier.case.skillName, skillVersion: dossier.case.skillVersion, candidateRange: dossier.case.candidateRange, acceptedTime: dossier.case.acceptedTime, confirmedTime: dossier.case.confirmedTime, completedAt: dossier.case.completedAt, closedReason: dossier.case.closedReason, evidenceCount: dossier.case.evidenceCount, turnCount: dossier.case.turnCount, }, evidence: scorable, confirmedEvidence, pendingEvidence, scorable, domainCounts, kindCounts, latestResult: dossier.latestResult ? { resultId: dossier.latestResult.resultId, candidates: dossier.latestResult.candidates, selectionAllowed: dossier.latestResult.selectionAllowed, confirmationAllowed: dossier.latestResult.confirmationAllowed, representativeTime: dossier.latestResult.representativeTime, selectedTime: dossier.latestResult.selectedTime, selectionKind: dossier.latestResult.selectionKind, algorithmVersion: dossier.latestResult.algorithmVersion, } : null, turns: dossier.turns.map((turn) => ({ id: turn.id, role: turn.role, text: turn.text })), }; } function assertCaseRef(input: { caseId?: unknown }): void { if (typeof input.caseId !== "string" || !uuidPattern.test(input.caseId)) { throw new RectificationToolServiceError("invalid_case_id"); } } function assertEvidenceRef(input: { evidenceId?: unknown }): string { if (typeof input.evidenceId !== "string" || !uuidPattern.test(input.evidenceId)) { throw new RectificationToolServiceError("invalid_evidence_id"); } return input.evidenceId; } function assertCandidateRef(input: { resultId?: unknown; candidateId?: unknown }): { resultId: string; candidateId: string } { if ( typeof input.resultId !== "string" || !uuidPattern.test(input.resultId) || typeof input.candidateId !== "string" || !timePattern.test(input.candidateId) ) { throw new RectificationToolServiceError("invalid_candidate_ref"); } return { resultId: input.resultId, candidateId: input.candidateId }; } function normalizeDatePart(value: string): string | null { const trimmed = value.trim(); if (!dateLike.test(trimmed)) return null; const parts = trimmed.split("-"); if (parts.length === 1) return `${parts[0]}-01-01`; if (parts.length === 2) return `${parts[0]}-${parts[1]!.padStart(2, "0")}-01`; return `${parts[0]}-${parts[1]!.padStart(2, "0")}-${parts[2]!.padStart(2, "0")}`; } export function createRectificationV9Tools(ctx: RectificationV9Context) { const { accounting, userId, caseId, turnId } = ctx; const engineVersion = v9EngineVersion(); const receipt = async ( toolName: string, publicPhase: string, status: "started" | "completed" | "failed" | "skipped", extra: { inputFingerprint?: string | null; resultFingerprint?: string | null; engineVersion?: string | null; safeErrorCode?: string | null; executedMethods?: readonly PublicRectificationMethod[] } = {}, ) => { try { await insertV9ToolReceipt(accounting, userId, caseId, turnId, { toolName, publicPhase, status, inputFingerprint: extra.inputFingerprint ?? null, resultFingerprint: extra.resultFingerprint ?? null, engineVersion: extra.engineVersion ?? null, safeErrorCode: extra.safeErrorCode ?? null, executedMethods: extra.executedMethods ?? [], }); } catch (error) { // Receipt persistence must never break the tool result; it is auditable // best-effort and surfaced in the run receipt. console.warn(`[rectification-v9] receipt write failed tool=${toolName} reason=${safeToolErrorCode(error)}`); } }; const hashResult = (value: unknown): string => { try { return canonicalToolInputFingerprint("result", { value: JSON.stringify(value) }); } catch { return ""; } }; const readCaseTool = createTool({ id: "rectification-read-case", description: "读取服务端 Case、证据摘要和权威出生上下文。birth_context 中的日期、地点、经纬度、IANA 时区、偏移、填报/当前时间及候选范围均来自服务端,必须原样使用,不得猜测或替换。任何一轮开始必须先调用本工具。input 只允许 caseId。", inputSchema: z.object({ caseId: z.string().uuid() }).strict(), execute: async (input) => { assertCaseRef(input); const inputFingerprint = canonicalToolInputFingerprint("rectification-read-case", input); await receipt("rectification-read-case", "case.loaded", "started", { inputFingerprint }); try { const dossier = await loadV9CaseDossier(accounting, userId, input.caseId); const compute = await loadV9CaseCompute(accounting, userId, input.caseId); const projection = safeCaseProjection(parseDossierForTools(dossier), compute); await receipt("rectification-read-case", "case.loaded", "completed", { inputFingerprint, resultFingerprint: hashResult(projection) }); return projection; } catch (error) { await receipt("rectification-read-case", "case.loaded", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) }); throw error; } }, }); const proposeEvidenceTool = createTool({ id: "rectification-propose-evidence", description: "提出一条事件证据草稿。quote 必须能规范化匹配当前用户轮次原文;日期只保留用户说过的精度(只说年份则 year);subject 只能 self/family/other;eventKind 使用受控枚举;summary 只复述用户原话,不得补充月份、原因、主动被动或人物关系。返回 evidenceId(服务器生成)。", inputSchema: z.object({ caseId: z.string().uuid(), quote: z.string().trim().min(2).max(400), proposedKind: z.string().trim().min(1).max(48), subject: z.enum(["self", "family", "other"]).default("self"), domain: z.string().trim().min(1).max(32), datePrecision: z.enum(["year", "month", "day", "range", "unknown"]), occurredFrom: z.string().trim().max(10).optional(), occurredTo: z.string().trim().max(10).optional(), summary: z.string().trim().min(1).max(1000), }).strict(), execute: async (input) => { assertCaseRef(input); if (!isEvidenceKind(input.proposedKind)) throw new RectificationToolServiceError("invalid_event_kind"); if (!isEvidenceDomain(input.domain)) throw new RectificationToolServiceError("invalid_domain"); if (!isDatePrecision(input.datePrecision)) throw new RectificationToolServiceError("invalid_date_precision"); const inputFingerprint = canonicalToolInputFingerprint("rectification-propose-evidence", { caseId: input.caseId, sourceTurnId: turnId, quote: input.quote, proposedKind: input.proposedKind, }); await receipt("rectification-propose-evidence", "evidence.proposed", "started", { inputFingerprint }); try { const occurredFrom = input.occurredFrom ? normalizeDatePart(input.occurredFrom) : null; const occurredTo = input.occurredTo ? normalizeDatePart(input.occurredTo) : null; const result = await proposeV9Evidence(accounting, userId, input.caseId, { sourceTurnId: turnId, quote: input.quote, subject: input.subject, eventKind: input.proposedKind, domain: input.domain, occurredFrom, occurredTo, datePrecision: input.datePrecision, summary: input.summary, }); await receipt("rectification-propose-evidence", "evidence.proposed", "completed", { inputFingerprint, resultFingerprint: hashResult(result), }); return { evidence_id: result.evidenceId, status: "draft", idempotent: result.idempotent, note: "草稿证据已记录。当前轮主动、明确、单一且无歧义的事件应继续调用 rectification-confirm-evidence;模糊、冲突或修订事件才等待用户补充或确认。", }; } catch (error) { await receipt("rectification-propose-evidence", "evidence.proposed", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) }); throw error; } }, }); const confirmEvidenceTool = createTool({ id: "rectification-confirm-evidence", description: "通过服务器确认路径确认既有证据。当前轮主动、明确、单一且无歧义的用户事件在 propose-evidence 成功后应同轮调用;用户明确确认既有 pending draft 时也可调用。不得确认助手文本、模型推断、历史摘要、模糊或冲突事实。", inputSchema: z.object({ caseId: z.string().uuid(), evidenceId: z.string().uuid(), }).strict(), execute: async (input) => { assertCaseRef(input); const evidenceId = assertEvidenceRef(input); const inputFingerprint = canonicalToolInputFingerprint("rectification-confirm-evidence", input); await receipt("rectification-confirm-evidence", "evidence.confirmed", "started", { inputFingerprint }); try { const result = await confirmV9Evidence(accounting, userId, input.caseId, evidenceId); const dossier = await loadV9CaseDossier(accounting, userId, input.caseId); if (isResumableStatus(dossier.case.status as RectificationCaseStatus)) { await transitionV9CaseStatus(accounting, userId, input.caseId, "collecting_evidence"); } await receipt("rectification-confirm-evidence", "evidence.confirmed", "completed", { inputFingerprint, resultFingerprint: hashResult(result), }); return { evidence_id: result.evidenceId, status: result.status, idempotent: result.idempotent, }; } catch (error) { await receipt("rectification-confirm-evidence", "evidence.confirmed", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) }); throw error; } }, }); const reviseEvidenceTool = createTool({ id: "rectification-revise-evidence", description: "修订既有证据的事实(通常是用户更正日期)。旧记录标记 superseded,生成新 revision,不覆盖历史。quote 必须来自用户本轮原话;日期精度如实保留。", inputSchema: z.object({ caseId: z.string().uuid(), evidenceId: z.string().uuid(), quote: z.string().trim().min(2).max(400), datePrecision: z.enum(["year", "month", "day", "range", "unknown"]), occurredFrom: z.string().trim().max(10).optional(), occurredTo: z.string().trim().max(10).optional(), summary: z.string().trim().min(1).max(1000), }).strict(), execute: async (input) => { assertCaseRef(input); const evidenceId = assertEvidenceRef(input); if (!isDatePrecision(input.datePrecision)) throw new RectificationToolServiceError("invalid_date_precision"); const inputFingerprint = canonicalToolInputFingerprint("rectification-revise-evidence", { caseId: input.caseId, evidenceId, quote: input.quote, datePrecision: input.datePrecision, }); await receipt("rectification-revise-evidence", "evidence.proposed", "started", { inputFingerprint }); try { const occurredFrom = input.occurredFrom ? normalizeDatePart(input.occurredFrom) : null; const occurredTo = input.occurredTo ? normalizeDatePart(input.occurredTo) : null; const result = await reviseV9Evidence(accounting, userId, input.caseId, { evidenceId, quote: input.quote, occurredFrom, occurredTo, datePrecision: input.datePrecision, summary: input.summary, }); await receipt("rectification-revise-evidence", "evidence.proposed", "completed", { inputFingerprint, resultFingerprint: hashResult(result), }); return { evidence_id: result.evidenceId, supersedes_evidence_id: result.supersedesEvidenceId, status: "pending_confirmation", idempotent: result.idempotent, }; } catch (error) { await receipt("rectification-revise-evidence", "evidence.proposed", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) }); throw error; } }, }); const compareCandidatesTool = createTool({ id: "rectification-compare-candidates", description: "用当前已确认证据运行候选比较。服务端按事件的实际领域选择分盘,并结合 Vimshottari、Narayana 与稳健性诊断。相同证据指纹会直接复用缓存,不重复计算。返回候选时间与相对支持度(不是概率),不返回原始分数与权重。", inputSchema: z.object({ caseId: z.string().uuid() }).strict(), execute: async (input) => { assertCaseRef(input); const inputFingerprint = canonicalToolInputFingerprint("rectification-compare-candidates", input); await receipt("rectification-compare-candidates", "candidates.comparing", "started", { inputFingerprint, engineVersion }); try { const dossier = await loadV9CaseDossier(accounting, userId, input.caseId); const parsed = parseDossierForTools(dossier); if (parsed.scorable.length === 0) { throw new RectificationToolServiceError("no_scorable_evidence"); } if (!parsed.case.candidateRange) throw new RectificationToolServiceError("case_range_missing"); const compute = await loadV9CaseCompute(accounting, userId, input.caseId); const evidenceFingerprint = evidenceLedgerFingerprint(dossier.evidence); const rangeFingerprint = candidateRangeFingerprint( parsed.case.candidateRange, compute.baselineProfileFingerprint, ); const events = toEngineEvents(scorableEvidence(dossier.evidence)); // Engine errors (including no_scorable_evidence after the V9 evidence // -> engine vocabulary mapping) must fail the tool honestly; cached // snapshots are only reused by the persist RPC's fingerprint cache. const score: V9EngineScoreResult = await runV9CandidateScore({ baselineBirthSnapshot: compute.baselineBirthSnapshot, candidateRange: parsed.case.candidateRange, events, }); const persisted = await persistV9Candidate(accounting, userId, input.caseId, { engineResultId: score.engineResultId, algorithmVersion: score.algorithmVersion, evidenceFingerprint, rangeFingerprint, skillVersion: RECTIFICATION_SKILL_VERSION, candidateRange: parsed.case.candidateRange, candidates: score.candidates, overallConfidence: score.overallConfidence, marginPercent: score.marginPercent, selectionAllowed: score.selectionAllowed, confirmationAllowed: score.confirmationAllowed, representativeTime: score.representativeTime, }); const projection = { result_id: persisted.resultId, cached: persisted.cached, candidate_range: parsed.case.candidateRange, candidates: persisted.candidates, overall_confidence: persisted.overallConfidence, selection_allowed: persisted.selectionAllowed, confirmation_allowed: persisted.confirmationAllowed, representative_time: persisted.representativeTime, algorithm_version: persisted.algorithmVersion, evidence_count: parsed.scorable.length, domain_count: Object.keys(parsed.domainCounts).length, }; await receipt("rectification-compare-candidates", "candidates.comparing", "completed", { inputFingerprint, resultFingerprint: hashResult(projection), engineVersion: persisted.algorithmVersion ?? engineVersion, executedMethods: score.executedMethods, }); return { ...projection, executed_methods: score.executedMethods }; } catch (error) { await receipt("rectification-compare-candidates", "candidates.comparing", "failed", { inputFingerprint, engineVersion, safeErrorCode: safeToolErrorCode(error), }); throw error; } }, }); const readDiagnosticsTool = createTool({ id: "rectification-read-diagnostics", description: "读取候选稳健性诊断:留一事件/留一领域保留率、日期敏感性、邻域稳定性、不稳定事件与最能区分候选的技法层。用于决定下一问补哪个方向。不返回原始分数。", inputSchema: z.object({ caseId: z.string().uuid() }).strict(), execute: async (input) => { assertCaseRef(input); const inputFingerprint = canonicalToolInputFingerprint("rectification-read-diagnostics", input); await receipt("rectification-read-diagnostics", "diagnostics.completed", "started", { inputFingerprint, engineVersion }); try { const dossier = await loadV9CaseDossier(accounting, userId, input.caseId); const parsed = parseDossierForTools(dossier); if (parsed.scorable.length === 0) { throw new RectificationToolServiceError("no_scorable_evidence"); } if (!parsed.case.candidateRange) throw new RectificationToolServiceError("case_range_missing"); const compute = await loadV9CaseCompute(accounting, userId, input.caseId); const diagnostics = await runV9Diagnostics({ baselineBirthSnapshot: compute.baselineBirthSnapshot, candidateRange: parsed.case.candidateRange, events: toEngineEvents(scorableEvidence(dossier.evidence)), }); const projection = { engine_result_id: diagnostics.engineResultId, algorithm_version: diagnostics.algorithmVersion, diagnostics: diagnostics.diagnostics, missing_layers: diagnostics.missingLayers, can_confirm_exact_minute: diagnostics.canConfirmExactMinute, }; await receipt("rectification-read-diagnostics", "diagnostics.completed", "completed", { inputFingerprint, resultFingerprint: hashResult(projection), engineVersion: diagnostics.algorithmVersion, executedMethods: diagnostics.executedMethods, }); return { ...projection, executed_methods: diagnostics.executedMethods }; } catch (error) { await receipt("rectification-read-diagnostics", "diagnostics.completed", "failed", { inputFingerprint, engineVersion, safeErrorCode: safeToolErrorCode(error), }); throw error; } }, }); const offerCandidatesTool = createTool({ id: "rectification-offer-candidates", description: "把已持久化的候选快照呈现给用户(当前候选/相对支持度,不是概率或确定性)。调用前必须已运行 compare-candidates。不会在同一回复中要求继续补证据。", inputSchema: z.object({ caseId: z.string().uuid() }).strict(), execute: async (input) => { assertCaseRef(input); const inputFingerprint = canonicalToolInputFingerprint("rectification-offer-candidates", input); await receipt("rectification-offer-candidates", "candidates.updated", "started", { inputFingerprint }); try { const dossier = await loadV9CaseDossier(accounting, userId, input.caseId); const parsed = parseDossierForTools(dossier); if (!parsed.latestResult) { throw new RectificationToolServiceError("no_candidate_result"); } const latest = parsed.latestResult; if (isResumableStatus(dossier.case.status as RectificationCaseStatus)) { await transitionV9CaseStatus(accounting, userId, input.caseId, "candidate_ready"); } const projection = { result_id: latest.resultId, candidates: latest.candidates, selection_allowed: latest.selectionAllowed, confirmation_allowed: latest.confirmationAllowed, representative_time: latest.representativeTime, selected_time: latest.selectedTime, selection_kind: latest.selectionKind, }; await receipt("rectification-offer-candidates", "candidates.updated", "completed", { inputFingerprint, resultFingerprint: hashResult(projection), }); return projection; } catch (error) { await receipt("rectification-offer-candidates", "candidates.updated", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) }); throw error; } }, }); const acceptCandidateTool = createTool({ id: "rectification-accept-candidate", description: "用户明确选择某个候选排盘时间后采用。服务器校验候选属于当前用户/Case/Result 快照且 profile 基线未变。采用状态是 accepted(“校正采用时间”),不是引擎确认。", inputSchema: z.object({ caseId: z.string().uuid(), resultId: z.string().uuid(), candidateId: z.string().regex(timePattern), }).strict(), execute: async (input) => { assertCaseRef(input); const { resultId } = assertCandidateRef({ resultId: input.resultId, candidateId: input.candidateId }); const inputFingerprint = canonicalToolInputFingerprint("rectification-accept-candidate", input); await receipt("rectification-accept-candidate", "candidate.accepted", "started", { inputFingerprint }); try { const result = await acceptV9Candidate(accounting, userId, input.caseId, resultId, input.candidateId); const projection = { saved_time: result.savedTime, status: result.status, case_status: result.caseStatus, result_id: result.resultId, idempotent: result.idempotent, }; await receipt("rectification-accept-candidate", "candidate.accepted", "completed", { inputFingerprint, resultFingerprint: hashResult(projection), }); return projection; } catch (error) { await receipt("rectification-accept-candidate", "candidate.accepted", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) }); throw error; } }, }); const confirmBirthTimeTool = createTool({ id: "rectification-confirm-birth-time", description: "仅在服务器确认门允许且用户本轮明确同意“就用 HH:MM / 确认这个时间”时调用。consentQuote 必须是用户原话片段,会在服务端做原文匹配。成功后写入 profile 并进入 confirmed 终态。", inputSchema: z.object({ caseId: z.string().uuid(), resultId: z.string().uuid(), candidateId: z.string().regex(timePattern), consentQuote: z.string().trim().min(2).max(400), }).strict(), execute: async (input) => { assertCaseRef(input); const { resultId } = assertCandidateRef({ resultId: input.resultId, candidateId: input.candidateId }); const inputFingerprint = canonicalToolInputFingerprint("rectification-confirm-birth-time", { caseId: input.caseId, resultId, candidateId: input.candidateId, }); await receipt("rectification-confirm-birth-time", "birth_time.confirmed", "started", { inputFingerprint }); try { const result = await confirmV9BirthTime(accounting, userId, input.caseId, { resultId, time: input.candidateId, consentQuote: input.consentQuote, sourceTurnId: turnId, }); const projection = { saved_time: result.savedTime, status: result.status, case_status: result.caseStatus, result_id: result.resultId, idempotent: result.idempotent, }; await receipt("rectification-confirm-birth-time", "birth_time.confirmed", "completed", { inputFingerprint, resultFingerprint: hashResult(projection), }); return projection; } catch (error) { await receipt("rectification-confirm-birth-time", "birth_time.confirmed", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) }); throw error; } }, }); const closeCaseTool = createTool({ id: "rectification-close-case", description: "用户主动结束本次校正(“先到这里/完成了”),Case 转 closed。这是用户选择的完成,不是引擎确认;不会写入 confirmed。", inputSchema: z.object({ caseId: z.string().uuid(), reason: z.enum(["completed_by_user", "abandoned_by_user", "other"]).default("completed_by_user"), }).strict(), execute: async (input) => { assertCaseRef(input); const inputFingerprint = canonicalToolInputFingerprint("rectification-close-case", input); await receipt("rectification-close-case", "run.completed", "started", { inputFingerprint }); try { const result = await closeV9Case(accounting, userId, input.caseId, input.reason); const projection = { case_id: result.caseId, status: result.status, idempotent: result.idempotent, }; await receipt("rectification-close-case", "run.completed", "completed", { inputFingerprint, resultFingerprint: hashResult(projection), }); return projection; } catch (error) { await receipt("rectification-close-case", "run.completed", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) }); throw error; } }, }); return { "rectification-read-case": readCaseTool, "rectification-propose-evidence": proposeEvidenceTool, "rectification-confirm-evidence": confirmEvidenceTool, "rectification-revise-evidence": reviseEvidenceTool, "rectification-compare-candidates": compareCandidatesTool, "rectification-read-diagnostics": readDiagnosticsTool, "rectification-offer-candidates": offerCandidatesTool, "rectification-accept-candidate": acceptCandidateTool, "rectification-confirm-birth-time": confirmBirthTimeTool, "rectification-close-case": closeCaseTool, }; } export type RectificationV9Tools = ReturnType; export const RECTIFICATION_V9_SKILL_NAME = RECTIFICATION_SKILL_NAME; export const RECTIFICATION_V9_SKILL_VERSION = RECTIFICATION_SKILL_VERSION; export { timePattern as rectificationV9TimePattern };