Files
Jyotisha/frontend/src/lib/rectification-agentic/v9/tool-service.ts
T
Jesse_Chen 909de8b884
Independent Staging Quality Gate / validate (push) Failing after 4m0s
Independent Staging Quality Gate / publish (push) Has been skipped
fix(web): persist rectification C/D answers on an append-only inference ledger
Engine result rows stay immutable. Choice answers append transitions, and reads overlay the latest revision instead of patching the cached receipt.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 08:22:01 +08:00

1627 lines
52 KiB
TypeScript

/**
* V9 rectification tool service.
*
* Server-owned fact layer behind the ten `rectification-*` tools. Every tool
* call authenticates through the Case row, validates ownership/status/skill
* version, reads the durable evidence ledger, derives canonical fingerprints,
* invokes the deterministic Python engine when evidence effectively changed,
* persists a candidate snapshot or receipt, and returns a compact safe
* projection. The model never supplies userId, birth data, candidate ranges,
* event arrays, scores or permission decisions.
*/
import { createHash } from "node:crypto";
import {
EVIDENCE_KINDS,
type EvidenceKind,
} from "./evidence-model";
import {
isPublicRectificationMethod,
isPublicRectificationPhase,
isPublicRectificationTool,
type PublicRectificationMethod,
type PublicRectificationPhase,
type PublicRectificationTool,
} from "./public-receipt";
import { RECTIFICATION_SKILL_VERSION } from "./case-status";
import {
asInferenceState,
type InferenceTransitionSnapshot,
} from "../core/compose-receipt";
import { INFERENCE_ALGORITHM_VERSION } from "../core/types";
import { decisionStateFingerprint } from "../core/decision-fingerprint";
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;
/**
* Minimal structural RPC client. SupabaseClient satisfies this; tests can
* pass a fake with a single rpc() method.
*/
export type RectificationRpcClient = {
rpc(
fn: string,
args: Record<string, unknown>,
): PromiseLike<{ data: unknown; error: { message: string } | null }>;
};
export type AccountingClient = RectificationRpcClient;
export class RectificationToolServiceError extends Error {
readonly code: string;
constructor(code: string) {
super(`Rectification tool service error: ${code}`);
this.name = "RectificationToolServiceError";
this.code = code;
}
}
function first(value: unknown): unknown {
if (Array.isArray(value)) return value[0] ?? null;
if (value && typeof value === "object" && "value" in value) {
return (value as { value?: unknown }).value;
}
return value;
}
function rpc<T>(
accounting: AccountingClient,
fn: string,
args: Record<string, unknown>,
): Promise<T> {
return Promise.resolve(accounting.rpc(fn, args)).then(({ data, error }) => {
if (error) throw new RectificationToolServiceError(error.message);
return first(data) as T;
});
}
export type ConversationFocusStatus =
| "active"
| "resolved"
| "declined"
| "skipped"
| "superseded";
export type ConversationFocus = Readonly<{
id: string;
caseId: string;
questionId: string;
intent: string;
targetEvidenceId: string | null;
targetDomain: string | null;
targetKind: string | null;
expectedAnswerSchema: Readonly<Record<string, unknown>>;
status: ConversationFocusStatus;
askedAt: string;
resolvedAt: string | null;
}>;
export type CaseConversationSummary = Readonly<{
confirmedEvidenceSummary: readonly Readonly<Record<string, unknown>>[];
pendingRevisions: readonly Readonly<Record<string, unknown>>[];
activeFocus: ConversationFocus | null;
declinedSkippedTopics: readonly Readonly<Record<string, unknown>>[];
candidateDivergenceSummary: Readonly<Record<string, unknown>> | null;
missingEvidenceCategories: readonly string[];
lastResultPolicy: Readonly<Record<string, unknown>> | null;
summaryVersion: number;
updatedAt: string;
}>;
export type V9CaseDossier = Readonly<{
case: Readonly<{
caseId: string;
sessionId: string;
status: 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;
lastActivityAt: string;
evidenceCount: number;
turnCount: number;
}>;
turns: readonly Readonly<{
id: string;
role: "user" | "assistant";
text: string | null;
status: string;
createdAt: string;
}>[];
evidence: readonly Readonly<{
id: string;
sourceTurnId: string;
subject: string;
eventKind: string;
domain: string;
occurredFrom: string | null;
occurredTo: string | null;
datePrecision: string;
summary: string;
status: string;
supersedesEvidenceId: string | null;
createdAt: string;
dateSource?: string | null;
dateReliability?: string | null;
dateCorroboration?: string | null;
dateConflictStatus?: string | null;
}>[];
conversationSummary: CaseConversationSummary;
latestResult: V9CandidateSnapshot | null;
}>;
export type RectificationCaseSkillIdentityStatus = Readonly<{
name: string;
version: string;
sha256: string | null;
sourceCommit: string | null;
status: "verified" | "legacy_unverifiable";
requiresSkillAdoption: boolean;
}>;
export type RectificationCaseSkillIdentity = Readonly<{
name: string;
version: string;
sha256: string;
sourceCommit: string | null;
}>;
export async function loadV9CaseSkillIdentityStatus(
accounting: AccountingClient,
userId: string,
caseId: string,
): Promise<RectificationCaseSkillIdentityStatus> {
const value = await rpc<Record<string, unknown>>(
accounting,
"get_agentic_rectification_skill_identity_status",
{ p_user_id: userId, p_case_id: caseId },
);
const name = rowText(value?.skill_name);
const version = rowText(value?.skill_version);
const sha256 = value?.skill_sha256 === null ? null : rowText(value?.skill_sha256);
const sourceCommit = value?.skill_source_commit === null ? null : rowText(value?.skill_source_commit);
const status = value?.skill_identity_status;
if (!name || !version || (status !== "verified" && status !== "legacy_unverifiable")
|| (sha256 !== null && (!sha256 || !/^[0-9a-f]{64}$/.test(sha256)))
|| (sourceCommit !== null && (!sourceCommit || !/^[0-9a-f]{40}$/.test(sourceCommit)))) {
throw new RectificationToolServiceError("agentic_rectification_invalid_skill_identity");
}
if (status === "verified" && sha256 === null) {
throw new RectificationToolServiceError("agentic_rectification_invalid_skill_identity");
}
if (status === "legacy_unverifiable" && sha256 !== null) {
throw new RectificationToolServiceError("agentic_rectification_invalid_skill_identity");
}
return {
name,
version,
sha256,
sourceCommit,
status,
requiresSkillAdoption: value?.requires_skill_adoption === true,
};
}
export async function loadV9CaseSkillIdentity(
accounting: AccountingClient,
userId: string,
caseId: string,
): Promise<RectificationCaseSkillIdentity> {
let value: Record<string, unknown>;
try {
value = await rpc<Record<string, unknown>>(
accounting,
"get_agentic_rectification_skill_identity",
{ p_user_id: userId, p_case_id: caseId },
);
} catch (error) {
if (error instanceof RectificationToolServiceError
&& error.code.includes("agentic_rectification_legacy_skill_identity_unverifiable")) {
throw error;
}
throw error;
}
const name = rowText(value?.skill_name);
const version = rowText(value?.skill_version);
const sha256 = rowText(value?.skill_sha256);
const sourceCommit = rowText(value?.skill_source_commit);
if (!name || !version || !sha256 || !/^[0-9a-f]{64}$/.test(sha256)
|| (sourceCommit !== null && !/^[0-9a-f]{40}$/.test(sourceCommit))) {
throw new RectificationToolServiceError("agentic_rectification_skill_identity_missing");
}
return { name, version, sha256, sourceCommit };
}
export async function insertV9SkillRunReceipt(
accounting: AccountingClient,
userId: string,
caseId: string,
turnId: string,
requestId: string,
runKind: "turn" | "regeneration",
identity: RectificationCaseSkillIdentity,
): Promise<void> {
await rpc<unknown>(
accounting,
"insert_agentic_rectification_skill_run_receipt",
{
p_user_id: userId,
p_case_id: caseId,
p_turn_id: turnId,
p_request_id: requestId,
p_run_kind: runKind,
p_skill_name: identity.name,
p_skill_version: identity.version,
p_skill_sha256: identity.sha256,
p_source_commit: identity.sourceCommit,
},
);
}
export type V9Candidate = Readonly<{
candidateId: string;
time: string;
rank: number;
relativeSupport: number;
tiedMinuteCount: number;
}>;
export type V9CandidateSnapshot = Readonly<{
resultId: string;
candidates: readonly V9Candidate[];
overallConfidence: "low" | "medium" | "high";
selectionAllowed: boolean;
confirmationAllowed: boolean;
representativeTime: string | null;
selectedTime: string | null;
selectionKind: string | null;
evidenceLedgerFingerprint: string | null;
candidateRangeFingerprint: string | null;
skillVersion: string | null;
algorithmVersion: string | null;
eventContractVersion: string | null;
policyVersion: string | null;
decisionReceipt: Readonly<Record<string, unknown>> | null;
executionLedger: readonly Readonly<Record<string, unknown>>[] | null;
createdAt: string;
invalidatedAt: string | null;
}>;
function timeValue(value: unknown): string | null {
const time = typeof value === "string" ? value.slice(0, 5) : "";
return /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(time) ? time : null;
}
function rowText(value: unknown): string | null {
return typeof value === "string" ? value : null;
}
function rowNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function rowBoolean(value: unknown): boolean {
return value === true;
}
function rowArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function rowObject(value: unknown): Readonly<Record<string, unknown>> | null {
return value && typeof value === "object" && !Array.isArray(value)
? value as Readonly<Record<string, unknown>>
: null;
}
function parseConversationFocus(value: unknown): ConversationFocus | null {
const row = rowObject(value);
if (!row || typeof row.id !== "string" || typeof row.case_id !== "string"
|| typeof row.question_id !== "string" || typeof row.intent !== "string") return null;
const status = row.status;
if (status !== "active" && status !== "resolved" && status !== "declined"
&& status !== "skipped" && status !== "superseded") return null;
return {
id: row.id,
caseId: row.case_id,
questionId: row.question_id,
intent: row.intent,
targetEvidenceId: rowText(row.target_evidence_id),
targetDomain: rowText(row.target_domain),
targetKind: rowText(row.target_kind),
expectedAnswerSchema: rowObject(row.expected_answer_schema) ?? {},
status,
askedAt: String(row.asked_at ?? ""),
resolvedAt: rowText(row.resolved_at),
};
}
function parseConversationSummary(value: unknown): CaseConversationSummary {
const row = rowObject(value) ?? {};
return {
confirmedEvidenceSummary: rowArray(row.confirmed_evidence_summary).flatMap((item) => {
const parsed = rowObject(item);
return parsed ? [parsed] : [];
}),
pendingRevisions: rowArray(row.pending_revisions).flatMap((item) => {
const parsed = rowObject(item);
return parsed ? [parsed] : [];
}),
activeFocus: parseConversationFocus(row.active_focus),
declinedSkippedTopics: rowArray(row.declined_skipped_topics).flatMap((item) => {
const parsed = rowObject(item);
return parsed ? [parsed] : [];
}),
candidateDivergenceSummary: rowObject(row.candidate_divergence_summary),
missingEvidenceCategories: rowArray(row.missing_evidence_categories)
.filter((item): item is string => typeof item === "string"),
lastResultPolicy: rowObject(row.last_result_policy),
summaryVersion: rowNumber(row.summary_version) ?? 1,
updatedAt: String(row.updated_at ?? ""),
};
}
export function parseV9CaseDossier(value: unknown): V9CaseDossier | null {
if (!value || typeof value !== "object") return null;
const root = value as Record<string, unknown>;
const caseRow = root.case && typeof root.case === "object"
? root.case as Record<string, unknown>
: null;
if (!caseRow || typeof caseRow.case_id !== "string") return null;
const range = caseRow.candidate_range && typeof caseRow.candidate_range === "object"
? caseRow.candidate_range as { start_time?: unknown; end_time?: unknown }
: null;
const candidateRange =
range && typeof range.start_time === "string" && typeof range.end_time === "string"
? { start_time: range.start_time, end_time: range.end_time }
: null;
const turns = rowArray(root.turns).flatMap((item): V9CaseDossier["turns"] => {
if (!item || typeof item !== "object") return [];
const turn = item as Record<string, unknown>;
if (typeof turn.id !== "string") return [];
return [{
id: turn.id,
role: turn.role === "user" ? "user" : "assistant",
text: rowText(turn.text),
status: String(turn.status ?? ""),
createdAt: String(turn.created_at ?? ""),
}];
});
const evidence = rowArray(root.evidence).flatMap((item): V9CaseDossier["evidence"] => {
if (!item || typeof item !== "object") return [];
const row = item as Record<string, unknown>;
if (typeof row.id !== "string") return [];
return [{
id: row.id,
sourceTurnId: String(row.source_turn_id ?? ""),
subject: String(row.subject ?? ""),
eventKind: String(row.event_kind ?? ""),
domain: String(row.domain ?? ""),
occurredFrom: rowText(row.occurred_from),
occurredTo: rowText(row.occurred_to),
datePrecision: String(row.date_precision ?? ""),
summary: String(row.summary ?? ""),
status: String(row.status ?? ""),
supersedesEvidenceId: rowText(row.supersedes_evidence_id),
createdAt: String(row.created_at ?? ""),
dateSource: rowText(row.date_source),
dateReliability: rowText(row.date_reliability),
dateCorroboration: rowText(row.date_corroboration),
dateConflictStatus: rowText(row.date_conflict_status),
}];
});
const latestResult = parseV9CandidateSnapshot(root.latest_result);
const conversationSummary = parseConversationSummary(root.conversation_summary);
return {
case: {
caseId: caseRow.case_id,
sessionId: String(caseRow.session_id ?? ""),
status: String(caseRow.status ?? ""),
skillName: String(caseRow.skill_name ?? ""),
skillVersion: String(caseRow.skill_version ?? ""),
candidateRange,
acceptedTime: timeValue(caseRow.accepted_time),
confirmedTime: timeValue(caseRow.confirmed_time),
completedAt: rowText(caseRow.completed_at),
closedReason: rowText(caseRow.closed_reason),
lastActivityAt: String(caseRow.last_activity_at ?? ""),
evidenceCount: rowNumber(caseRow.evidence_count) ?? 0,
turnCount: rowNumber(caseRow.turn_count) ?? 0,
},
turns,
evidence,
conversationSummary,
latestResult,
};
}
export function parseV9CandidateSnapshot(value: unknown): V9CandidateSnapshot | null {
const row = rowObject(value);
const resultId = rowText(row?.result_id);
if (!row || !resultId || !Array.isArray(row.candidates) || row.candidates.length === 0) return null;
const candidates: V9Candidate[] = [];
const seenIds = new Set<string>();
for (const item of row.candidates) {
const candidate = rowObject(item);
const candidateId = rowText(candidate?.candidate_id);
const candidateTime = timeValue(candidate?.time);
const rank = rowNumber(candidate?.rank);
const relativeSupport = rowNumber(candidate?.relative_support);
const tiedMinuteCount = rowNumber(candidate?.tied_minute_count);
if (
!candidate || !candidateId || !uuidPattern.test(candidateId) || seenIds.has(candidateId)
|| !candidateTime
|| rank === null || !Number.isInteger(rank) || rank < 1
|| relativeSupport === null || !Number.isInteger(relativeSupport) || relativeSupport < 0 || relativeSupport > 100
|| tiedMinuteCount === null || !Number.isInteger(tiedMinuteCount) || tiedMinuteCount < 1
) return null;
seenIds.add(candidateId);
candidates.push({ candidateId, time: candidateTime, rank, relativeSupport, tiedMinuteCount });
}
const decisionReceipt = rowObject(row.decision_receipt);
const executionLedger = Array.isArray(row.execution_ledger)
&& row.execution_ledger.every((item) => rowObject(item) !== null)
? row.execution_ledger as readonly Readonly<Record<string, unknown>>[]
: null;
return {
resultId,
candidates,
overallConfidence: row.overall_confidence === "high" || row.overall_confidence === "medium" ? row.overall_confidence : "low",
selectionAllowed: rowBoolean(row.selection_allowed),
confirmationAllowed: rowBoolean(row.confirmation_allowed),
representativeTime: timeValue(row.representative_time),
selectedTime: timeValue(row.selected_time),
selectionKind: rowText(row.selection_kind),
evidenceLedgerFingerprint: rowText(row.evidence_ledger_fingerprint),
candidateRangeFingerprint: rowText(row.candidate_range_fingerprint),
skillVersion: rowText(row.skill_version),
algorithmVersion: rowText(row.algorithm_version),
eventContractVersion: rowText(row.event_contract_version),
policyVersion: rowText(row.decision_policy_version) ?? rowText(decisionReceipt?.policy_version),
decisionReceipt,
executionLedger,
createdAt: String(row.created_at ?? ""),
invalidatedAt: rowText(row.invalidated_at),
};
}
export type V9ComputeProjection = Readonly<{
caseId: string;
skillVersion: string;
baselineProfileFingerprint: string;
baselineBirthSnapshot: Readonly<Record<string, unknown>>;
candidateRange: { start_time: string; end_time: string };
}>;
export function parseV9ComputeProjection(value: unknown): V9ComputeProjection | null {
if (!value || typeof value !== "object") return null;
const row = value as Record<string, unknown>;
const range = row.candidate_range && typeof row.candidate_range === "object"
? row.candidate_range as { start_time?: unknown; end_time?: unknown }
: null;
const snapshot = row.baseline_birth_snapshot && typeof row.baseline_birth_snapshot === "object"
? row.baseline_birth_snapshot as Record<string, unknown>
: null;
if (
typeof row.case_id !== "string"
|| typeof row.skill_version !== "string"
|| typeof row.baseline_profile_fingerprint !== "string"
|| !snapshot
|| !range
|| typeof range.start_time !== "string"
|| typeof range.end_time !== "string"
) {
return null;
}
return {
caseId: row.case_id,
skillVersion: row.skill_version,
baselineProfileFingerprint: row.baseline_profile_fingerprint,
baselineBirthSnapshot: snapshot,
candidateRange: { start_time: range.start_time, end_time: range.end_time },
};
}
export async function loadV9CaseDossier(
accounting: AccountingClient,
userId: string,
caseId: string,
): Promise<V9CaseDossier> {
const row = await rpc<unknown>(
accounting,
"get_agentic_rectification_case_dossier",
{ p_user_id: userId, p_case_id: caseId },
);
const dossier = parseV9CaseDossier(row);
if (!dossier) throw new RectificationToolServiceError("invalid_case_dossier");
return dossier;
}
export async function loadV9CaseCompute(
accounting: AccountingClient,
userId: string,
caseId: string,
): Promise<V9ComputeProjection> {
const row = await rpc<unknown>(
accounting,
"get_agentic_rectification_case_compute",
{ p_user_id: userId, p_case_id: caseId },
);
const projection = parseV9ComputeProjection(row);
if (!projection) throw new RectificationToolServiceError("invalid_case_compute");
return projection;
}
/** Evidence rows that actually carry a scorable date. */
export function scorableEvidence(
evidence: V9CaseDossier["evidence"],
): V9CaseDossier["evidence"] {
return evidence.filter(
(item) =>
item.status === "confirmed"
&& item.datePrecision !== "unknown"
&& (item.occurredFrom || item.occurredTo),
);
}
function hashParts(parts: readonly string[]): string {
return createHash("sha256").update(parts.join("|")).digest("hex");
}
/** Canonical evidence ledger fingerprint over scorable evidence rows. */
export function evidenceLedgerFingerprint(
evidence: V9CaseDossier["evidence"],
): string {
const rows = [...scorableEvidence(evidence)]
.sort((left, right) => left.id.localeCompare(right.id))
.map((item) =>
[
item.id,
item.eventKind,
item.domain,
item.occurredFrom ?? "",
item.occurredTo ?? "",
item.datePrecision,
item.summary,
].join("::"),
);
return hashParts(["v9-evidence-ledger-v1", ...rows]);
}
/** Candidate range fingerprint: range + baseline fingerprint. */
export function candidateRangeFingerprint(
range: { start_time: string; end_time: string },
baselineProfileFingerprint: string,
): string {
return hashParts([
"v9-candidate-range-v1",
range.start_time,
range.end_time,
baselineProfileFingerprint,
]);
}
/** Canonical tool input fingerprint for receipts. */
export function canonicalToolInputFingerprint(
toolName: string,
args: Readonly<Record<string, unknown>>,
): string {
const safe = Object.fromEntries(
Object.entries(args)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, value]) => [key, typeof value === "string" ? value : JSON.stringify(value)]),
);
return hashParts([`v9-tool:${toolName}`, JSON.stringify(safe)]);
}
export type V10RunAttemptStatus = "completed" | "failed" | "retryable" | "aborted";
export type V10RunAttemptClaim = Readonly<{
attemptId: string;
status: string;
shouldExecute: boolean;
alreadyInProgress: boolean;
}>;
export async function createV10RunAttempt(
accounting: AccountingClient,
userId: string,
caseId: string,
turnId: string,
attemptNumber: number,
): Promise<V10RunAttemptClaim> {
const row = await rpc<{
attempt_id?: unknown;
status?: unknown;
should_execute?: unknown;
already_in_progress?: unknown;
}>(
accounting,
"create_agentic_rectification_run_attempt",
{
p_user_id: userId,
p_case_id: caseId,
p_turn_id: turnId,
p_attempt_number: attemptNumber,
},
);
const attemptId = rowText(row?.attempt_id);
const status = rowText(row?.status) ?? "started";
if (!attemptId) throw new RectificationToolServiceError("invalid_attempt_id");
const legacyIdempotent = rowBoolean((row as { idempotent?: unknown } | null)?.idempotent);
return {
attemptId,
status,
shouldExecute: row?.should_execute === undefined
? !legacyIdempotent
: rowBoolean(row.should_execute),
alreadyInProgress: row?.already_in_progress === undefined
? legacyIdempotent && status === "started"
: rowBoolean(row.already_in_progress),
};
}
export async function finalizeV10RunAttempt(
accounting: AccountingClient,
userId: string,
caseId: string,
turnId: string,
attemptId: string,
status: V10RunAttemptStatus,
errorCode: string | null,
usage: Readonly<Record<string, unknown>>,
): Promise<void> {
await rpc<unknown>(
accounting,
"finalize_agentic_rectification_run_attempt",
{
p_user_id: userId,
p_case_id: caseId,
p_turn_id: turnId,
p_attempt_id: attemptId,
p_status: status,
p_error_code: errorCode,
p_usage: usage,
},
);
}
export type V9TurnAppendResult = Readonly<{ turnId: string }>;
export async function appendV9Turn(
accounting: AccountingClient,
userId: string,
caseId: string,
input: {
userMessage: string | null;
modelName: string;
modelVersion?: string;
},
): Promise<V9TurnAppendResult> {
const row = await rpc<{ turn_id?: unknown }>(
accounting,
"append_agentic_rectification_turn",
{
p_user_id: userId,
p_case_id: caseId,
p_user_message: input.userMessage,
p_assistant_message: null,
p_model_name: input.modelName,
p_model_version: input.modelVersion ?? null,
p_status: "pending",
},
);
const turnId = typeof row?.turn_id === "string" ? row.turn_id : "";
if (!turnId) throw new RectificationToolServiceError("invalid_turn_id");
return { turnId };
}
export async function finalizeV9Turn(
accounting: AccountingClient,
userId: string,
caseId: string,
turnId: string,
status: "completed" | "failed" | "retryable",
assistantMessage: string | null,
): Promise<void> {
await rpc<unknown>(
accounting,
"finalize_agentic_rectification_turn",
{
p_user_id: userId,
p_case_id: caseId,
p_turn_id: turnId,
p_status: status,
p_assistant_message: assistantMessage,
},
);
}
export async function insertV9ToolReceipt(
accounting: AccountingClient,
userId: string,
caseId: string,
turnId: string,
input: {
toolName: string;
publicPhase: string;
status: "started" | "completed" | "failed" | "skipped";
inputFingerprint?: string | null;
resultFingerprint?: string | null;
engineVersion?: string | null;
safeErrorCode?: string | null;
executedMethods?: readonly PublicRectificationMethod[];
attemptId?: string | null;
},
): Promise<void> {
if (!isPublicRectificationTool(input.toolName)) {
throw new RectificationToolServiceError("tool_not_allowlisted");
}
if (!isPublicRectificationPhase(input.publicPhase)) {
throw new RectificationToolServiceError("phase_not_allowlisted");
}
const executedMethods = [...new Set(input.executedMethods ?? [])];
if (!executedMethods.every(isPublicRectificationMethod)) {
throw new RectificationToolServiceError("method_not_allowlisted");
}
await rpc<unknown>(
accounting,
"insert_agentic_rectification_tool_receipt",
{
p_user_id: userId,
p_case_id: caseId,
p_turn_id: turnId,
p_tool_name: input.toolName,
p_public_phase: input.publicPhase,
p_status: input.status,
p_input_fingerprint: input.inputFingerprint ?? null,
p_result_fingerprint: input.resultFingerprint ?? null,
p_engine_version: input.engineVersion ?? null,
p_safe_error_code: input.safeErrorCode ?? null,
p_executed_methods: executedMethods,
p_attempt_id: input.attemptId ?? null,
},
);
}
export async function insertV9RunPhase(
accounting: AccountingClient,
userId: string,
caseId: string,
turnId: string,
phase: string,
toolName: string | null,
sequence: number,
attemptId?: string | null,
): Promise<void> {
if (!isPublicRectificationPhase(phase)) {
throw new RectificationToolServiceError("phase_not_allowlisted");
}
await rpc<unknown>(
accounting,
"insert_agentic_rectification_run_phase",
{
p_user_id: userId,
p_case_id: caseId,
p_turn_id: turnId,
p_phase: phase,
p_tool_name: toolName,
p_sequence: sequence,
p_attempt_id: attemptId ?? null,
},
);
}
export type V9TurnReceipt = Readonly<{
turnId: string;
attemptId: string | null;
status: string;
skillName: string;
skillVersion: string;
engineVersion: string | null;
phases: readonly Readonly<{ phase: string; tool: string | null }>[];
toolActivities: readonly Readonly<{
tool: PublicRectificationTool;
status: "completed" | "failed";
methods: readonly PublicRectificationMethod[];
}>[];
tools: readonly string[];
methods: readonly PublicRectificationMethod[];
startedAt: string;
completedAt: string | null;
}>;
export async function loadV9TurnReceipt(
accounting: AccountingClient,
userId: string,
caseId: string,
turnId: string,
): Promise<V9TurnReceipt | null> {
const row = await rpc<Record<string, unknown>>(
accounting,
"get_agentic_rectification_turn_receipt",
{ p_user_id: userId, p_case_id: caseId, p_turn_id: turnId },
);
if (!row || typeof row.turn_id !== "string") return null;
const phases = rowArray(row.phases).flatMap((item) => {
if (!item || typeof item !== "object") return [];
const phaseRow = item as Record<string, unknown>;
return [{ phase: String(phaseRow.phase ?? ""), tool: rowText(phaseRow.tool) }];
});
const toolActivities = rowArray(row.tool_activities)
.flatMap<V9TurnReceipt["toolActivities"][number]>((item) => {
const activity = rowObject(item);
if (!activity) return [];
const tool = activity.tool;
const status = activity.status;
if (!isPublicRectificationTool(tool) || (status !== "completed" && status !== "failed")) return [];
return [{
tool,
status,
methods: status === "completed"
? rowArray(activity.methods).filter(isPublicRectificationMethod)
: [],
}];
});
return {
turnId: row.turn_id,
attemptId: rowText(row.attempt_id),
status: String(row.status ?? ""),
skillName: String(row.skill_name ?? ""),
skillVersion: String(row.skill_version ?? ""),
engineVersion: rowText(row.engine_version),
phases,
toolActivities,
tools: rowArray(row.tools).map((item) => String(item)),
methods: rowArray(row.methods).filter(isPublicRectificationMethod),
startedAt: String(row.started_at ?? ""),
completedAt: rowText(row.completed_at),
};
}
/** Map a persisted turn status to the public receipt status vocabulary. */
export function receiptStatusFromTurn(status: string): "completed" | "degraded" | "blocked" | "failed" {
if (status === "completed") return "completed";
if (status === "retryable") return "degraded";
return "failed";
}
export type ProposeEvidenceResult = Readonly<{
evidenceId: string | null;
idempotent: boolean;
outcome: "accepted" | "rejected";
errorCode: string | null;
status: string;
}>;
export async function proposeV9Evidence(
accounting: AccountingClient,
userId: string,
caseId: string,
input: {
sourceTurnId: string;
quote: string;
subject: "self" | "family" | "other";
eventKind: EvidenceKind;
domain: string;
occurredFrom: string | null;
occurredTo: string | null;
datePrecision: string;
summary: string;
},
): Promise<ProposeEvidenceResult> {
const row = await rpc<{
evidence_id?: unknown;
idempotent?: unknown;
outcome?: unknown;
error_code?: unknown;
status?: unknown;
}>(
accounting,
"propose_agentic_rectification_evidence",
{
p_user_id: userId,
p_case_id: caseId,
p_source_turn_id: input.sourceTurnId,
p_user_quote: input.quote,
p_subject: input.subject,
p_event_kind: input.eventKind,
p_domain: input.domain,
p_occurred_from: input.occurredFrom,
p_occurred_to: input.occurredTo,
p_date_precision: input.datePrecision,
p_summary: input.summary,
},
);
const errorCode = rowText(row?.error_code);
const evidenceId = typeof row?.evidence_id === "string" ? row.evidence_id : null;
if (errorCode || !evidenceId) {
return {
evidenceId: null,
idempotent: false,
outcome: "rejected",
errorCode: errorCode ?? "invalid_item",
status: String(row?.status ?? "rejected"),
};
}
return {
evidenceId,
idempotent: row?.idempotent === true,
outcome: "accepted",
errorCode: null,
status: String(row?.status ?? "draft"),
};
}
export async function confirmV9Evidence(
accounting: AccountingClient,
userId: string,
caseId: string,
evidenceId: string,
): Promise<Readonly<{ evidenceId: string; status: string; idempotent: boolean }>> {
const row = await rpc<{ evidence_id?: unknown; status?: unknown; idempotent?: unknown }>(
accounting,
"confirm_agentic_rectification_evidence",
{ p_user_id: userId, p_case_id: caseId, p_evidence_id: evidenceId },
);
const confirmedId = typeof row?.evidence_id === "string" ? row.evidence_id : "";
if (!confirmedId) throw new RectificationToolServiceError("invalid_evidence_id");
return {
evidenceId: confirmedId,
status: String(row?.status ?? "confirmed"),
idempotent: row?.idempotent === true,
};
}
export async function reviseV9Evidence(
accounting: AccountingClient,
userId: string,
caseId: string,
input: {
evidenceId: string;
quote: string;
occurredFrom: string | null;
occurredTo: string | null;
datePrecision: string;
summary: string;
},
): Promise<Readonly<{ evidenceId: string; supersedesEvidenceId: string; idempotent: boolean }>> {
const row = await rpc<{ evidence_id?: unknown; supersedes_evidence_id?: unknown; idempotent?: unknown }>(
accounting,
"revise_agentic_rectification_evidence",
{
p_user_id: userId,
p_case_id: caseId,
p_evidence_id: input.evidenceId,
p_user_quote: input.quote,
p_occurred_from: input.occurredFrom,
p_occurred_to: input.occurredTo,
p_date_precision: input.datePrecision,
p_summary: input.summary,
},
);
const evidenceId = typeof row?.evidence_id === "string" ? row.evidence_id : "";
const supersedes = typeof row?.supersedes_evidence_id === "string" ? row.supersedes_evidence_id : "";
if (!evidenceId) throw new RectificationToolServiceError("invalid_evidence_id");
return { evidenceId, supersedesEvidenceId: supersedes, idempotent: row?.idempotent === true };
}
export async function setV10ConversationFocus(
accounting: AccountingClient,
userId: string,
caseId: string,
input: {
questionId: string;
intent: string;
targetEvidenceId?: string | null;
targetDomain?: string | null;
targetKind?: string | null;
expectedAnswerSchema?: Readonly<Record<string, unknown>>;
},
): Promise<Readonly<{ focus: ConversationFocus; idempotent: boolean }>> {
const row = await rpc<Record<string, unknown>>(
accounting,
"set_agentic_rectification_conversation_focus",
{
p_user_id: userId,
p_case_id: caseId,
p_question_id: input.questionId,
p_intent: input.intent,
p_target_evidence_id: input.targetEvidenceId ?? null,
p_target_domain: input.targetDomain ?? null,
p_target_kind: input.targetKind ?? null,
p_expected_answer_schema: input.expectedAnswerSchema ?? {},
},
);
const focus = parseConversationFocus(row.focus ?? row);
if (!focus) throw new RectificationToolServiceError("invalid_focus");
return { focus, idempotent: row.idempotent === true };
}
export async function resolveV10ConversationFocus(
accounting: AccountingClient,
userId: string,
caseId: string,
input: {
focusId: string;
status: "resolved" | "declined" | "skipped";
evidenceId?: string | null;
},
): Promise<Readonly<{ focusId: string; status: string; evidenceId: string | null; idempotent: boolean }>> {
const row = await rpc<Record<string, unknown>>(
accounting,
"resolve_agentic_rectification_conversation_focus",
{
p_user_id: userId,
p_case_id: caseId,
p_focus_id: input.focusId,
p_status: input.status,
p_evidence_id: input.evidenceId ?? null,
},
);
const focusId = rowText(row.focus_id);
if (!focusId) throw new RectificationToolServiceError("invalid_focus");
return {
focusId,
status: String(row.status ?? input.status),
evidenceId: rowText(row.evidence_id),
idempotent: row.idempotent === true,
};
}
export async function confirmV10Evidence(
accounting: AccountingClient,
userId: string,
caseId: string,
focusId: string | null,
evidenceId: string,
): Promise<Readonly<{ focusId: string | null; evidenceId: string; status: string; idempotent: boolean }>> {
const row = await rpc<Record<string, unknown>>(
accounting,
"confirm_agentic_rectification_evidence_v10",
{
p_user_id: userId,
p_case_id: caseId,
p_focus_id: focusId,
p_evidence_id: evidenceId,
},
);
const confirmedId = rowText(row.evidence_id);
if (!confirmedId) throw new RectificationToolServiceError("invalid_evidence_id");
return {
focusId: rowText(row.focus_id),
evidenceId: confirmedId,
status: String(row.status ?? "confirmed"),
idempotent: row.idempotent === true,
};
}
export async function reviseV10Evidence(
accounting: AccountingClient,
userId: string,
caseId: string,
input: {
focusId: string;
evidenceId: string;
quote: string;
occurredFrom: string | null;
occurredTo: string | null;
datePrecision: string;
summary: string;
},
): Promise<Readonly<{ focusId: string; evidenceId: string; supersedesEvidenceId: string; idempotent: boolean }>> {
const row = await rpc<Record<string, unknown>>(
accounting,
"revise_agentic_rectification_evidence_v10",
{
p_user_id: userId,
p_case_id: caseId,
p_focus_id: input.focusId,
p_evidence_id: input.evidenceId,
p_user_quote: input.quote,
p_occurred_from: input.occurredFrom,
p_occurred_to: input.occurredTo,
p_date_precision: input.datePrecision,
p_summary: input.summary,
},
);
const evidenceId = rowText(row.evidence_id);
const supersedesEvidenceId = rowText(row.supersedes_evidence_id);
if (!evidenceId || !supersedesEvidenceId) {
throw new RectificationToolServiceError("invalid_evidence_id");
}
return {
focusId: rowText(row.focus_id) ?? input.focusId,
evidenceId,
supersedesEvidenceId,
idempotent: row.idempotent === true,
};
}
export type V10EvidenceBatchItem = Readonly<{
quote: string;
subject: "self" | "family" | "other";
eventKind: EvidenceKind;
domain: string;
occurredFrom: string | null;
occurredTo: string | null;
datePrecision: string;
summary: string;
}>;
export type V10EvidenceBatchResult = Readonly<{
items: readonly Readonly<{
index: number;
outcome: "accepted" | "needs_clarification" | "rejected";
evidenceId: string | null;
status: string;
idempotent: boolean;
clarificationFields: readonly string[];
errorCode: string | null;
}>[];
acceptedCount: number;
needsClarificationCount: number;
rejectedCount: number;
focusId: string | null;
}>;
export async function recordV10EvidenceBatch(
accounting: AccountingClient,
userId: string,
caseId: string,
sourceTurnId: string,
focusId: string | null,
items: readonly V10EvidenceBatchItem[],
): Promise<V10EvidenceBatchResult> {
const rpcItems = items.map((item) => ({
idempotency_key: hashParts([
"v10-evidence-item-v1",
caseId,
sourceTurnId,
JSON.stringify({
quote: item.quote,
subject: item.subject,
event_kind: item.eventKind,
domain: item.domain,
occurred_from: item.occurredFrom,
occurred_to: item.occurredTo,
date_precision: item.datePrecision,
summary: item.summary,
}),
]),
quote: item.quote,
subject: item.subject,
event_kind: item.eventKind,
domain: item.domain,
occurred_from: item.occurredFrom,
occurred_to: item.occurredTo,
date_precision: item.datePrecision,
summary: item.summary,
}));
const row = await rpc<Record<string, unknown>>(
accounting,
"record_agentic_rectification_evidence_batch",
{
p_user_id: userId,
p_case_id: caseId,
p_source_turn_id: sourceTurnId,
p_focus_id: focusId,
p_items: rpcItems,
},
);
const results = rowArray(row.items).flatMap((value) => {
const item = rowObject(value);
if (!item) return [];
const outcome = item.outcome;
if (outcome !== "accepted" && outcome !== "needs_clarification" && outcome !== "rejected") return [];
return [{
index: rowNumber(item.index) ?? 0,
outcome: outcome as V10EvidenceBatchResult["items"][number]["outcome"],
evidenceId: rowText(item.evidence_id),
status: String(item.status ?? ""),
idempotent: item.idempotent === true,
clarificationFields: rowArray(item.clarification_fields)
.filter((field): field is string => typeof field === "string"),
errorCode: rowText(item.error_code),
}];
});
return {
items: results,
acceptedCount: rowNumber(row.accepted_count) ?? results.filter((item) => item.outcome === "accepted").length,
needsClarificationCount: rowNumber(row.needs_clarification_count)
?? results.filter((item) => item.outcome === "needs_clarification").length,
rejectedCount: rowNumber(row.rejected_count) ?? results.filter((item) => item.outcome === "rejected").length,
focusId: rowText(row.focus_id),
};
}
export async function transitionV9CaseStatus(
accounting: AccountingClient,
userId: string,
caseId: string,
status: "draft" | "collecting_evidence" | "candidate_ready" | "candidate_accepted" | "needs_rebaseline" | "paused",
): Promise<Readonly<{ status: string; idempotent: boolean }>> {
const row = await rpc<{ status?: unknown; idempotent?: unknown }>(
accounting,
"transition_agentic_rectification_case_status",
{ p_user_id: userId, p_case_id: caseId, p_status: status },
);
return { status: String(row?.status ?? status), idempotent: row?.idempotent === true };
}
export type V9PersistedCandidate = Readonly<{
resultId: string;
cached: boolean;
candidates: V9CandidateSnapshot["candidates"];
overallConfidence: "low" | "medium" | "high";
selectionAllowed: boolean;
confirmationAllowed: boolean;
representativeTime: string | null;
algorithmVersion: string | null;
eventContractVersion: string | null;
policyVersion: string | null;
decisionReceipt: Readonly<Record<string, unknown>>;
executionLedger: readonly Readonly<Record<string, unknown>>[];
}>;
export async function persistV9Candidate(
accounting: AccountingClient,
userId: string,
caseId: string,
input: {
engineResultId: string;
algorithmVersion: string;
evidenceFingerprint: string;
rangeFingerprint: string;
skillVersion: string;
eventContractVersion: string;
policyVersion: string;
candidateRange: { start_time: string; end_time: string };
candidates: V9CandidateSnapshot["candidates"];
decisionReceipt: Readonly<Record<string, unknown>>;
executionLedger: readonly Readonly<Record<string, unknown>>[];
},
): Promise<V9PersistedCandidate> {
const row = await rpc<Record<string, unknown>>(
accounting,
"persist_agentic_rectification_candidate_v2",
{
p_user_id: userId,
p_case_id: caseId,
p_engine_result_id: input.engineResultId,
p_evidence_ledger_fingerprint: input.evidenceFingerprint,
p_candidate_range_fingerprint: input.rangeFingerprint,
p_skill_version: input.skillVersion,
p_algorithm_version: input.algorithmVersion,
p_event_contract_version: input.eventContractVersion,
p_decision_policy_version: input.policyVersion,
p_candidate_range: input.candidateRange,
p_candidates: input.candidates.map((candidate) => ({
candidate_id: candidate.candidateId,
time: candidate.time,
rank: candidate.rank,
relative_support: candidate.relativeSupport,
tied_minute_count: candidate.tiedMinuteCount,
})),
p_decision_receipt: input.decisionReceipt,
p_execution_ledger: input.executionLedger,
},
);
const snapshot = parseV9CandidateSnapshot(row);
if (!snapshot) throw new RectificationToolServiceError("invalid_candidate_result");
return {
resultId: snapshot.resultId,
cached: row.cached === true,
candidates: snapshot.candidates,
overallConfidence: snapshot.overallConfidence,
selectionAllowed: snapshot.selectionAllowed,
confirmationAllowed: snapshot.confirmationAllowed,
representativeTime: snapshot.representativeTime,
algorithmVersion: snapshot.algorithmVersion ?? input.algorithmVersion,
eventContractVersion: snapshot.eventContractVersion ?? input.eventContractVersion,
policyVersion: snapshot.policyVersion ?? input.policyVersion,
decisionReceipt: snapshot.decisionReceipt ?? input.decisionReceipt,
executionLedger: snapshot.executionLedger ?? input.executionLedger,
};
}
function parseTransitionSnapshot(value: unknown): InferenceTransitionSnapshot | null {
const row = rowObject(value);
const inference = asInferenceState(row?.inference_state);
const resultId = rowText(row?.result_id);
const fingerprint = rowText(row?.decision_state_fingerprint);
const revision = rowNumber(row?.revision);
if (!row || !inference || !resultId || !fingerprint || revision === null) return null;
return {
id: rowText(row.id) ?? undefined,
resultId,
revision,
probeId: rowText(row.probe_id),
reason: String(row.reason ?? "choice"),
idempotent: row.idempotent === true,
decisionStateFingerprint: fingerprint,
inferenceState: inference,
posteriorBefore: rowObject(row.posterior_before) as Record<string, number> ?? {},
posteriorAfter: rowObject(row.posterior_after) as Record<string, number> ?? {},
scoreDeltas: rowObject(row.score_deltas) as Record<string, number> ?? {},
};
}
export async function loadLatestInferenceTransition(
accounting: AccountingClient,
userId: string,
caseId: string,
): Promise<InferenceTransitionSnapshot | null> {
const row = await rpc<unknown>(
accounting,
"get_agentic_rectification_latest_inference_transition",
{ p_user_id: userId, p_case_id: caseId },
);
return parseTransitionSnapshot(row);
}
export type PersistInferenceTransitionInput = Readonly<{
expectedRevision: number;
probeId: string;
openProbeId: string;
semanticKey: string;
candidateSplitHash: string;
answerClass: string;
rawAnswer: string;
inferenceState: Readonly<Record<string, unknown>>;
posteriorBefore: Readonly<Record<string, number>>;
posteriorAfter: Readonly<Record<string, number>>;
scoreDeltas: Readonly<Record<string, number>>;
decisionStateFingerprint: string;
reason: "choice" | "supersede" | "already_answered";
idempotencyKey: string;
candidateSetId: string;
}>;
export async function persistV9InferenceState(
accounting: AccountingClient,
userId: string,
caseId: string,
input: PersistInferenceTransitionInput,
): Promise<Readonly<{
resultId: string;
revision: number;
idempotent: boolean;
decisionReceipt: Readonly<Record<string, unknown>>;
decisionStateFingerprint: string;
}>> {
const row = await rpc<Record<string, unknown>>(
accounting,
"append_agentic_rectification_inference_transition",
{
p_user_id: userId,
p_case_id: caseId,
p_expected_revision: input.expectedRevision,
p_probe_id: input.probeId,
p_open_probe_id: input.openProbeId,
p_semantic_key: input.semanticKey,
p_candidate_split_hash: input.candidateSplitHash,
p_answer_class: input.answerClass,
p_raw_answer: input.rawAnswer,
p_inference_state: input.inferenceState,
p_posterior_before: input.posteriorBefore,
p_posterior_after: input.posteriorAfter,
p_score_deltas: input.scoreDeltas,
p_decision_state_fingerprint: input.decisionStateFingerprint,
p_reason: input.reason,
p_idempotency_key: input.idempotencyKey,
p_candidate_set_id: input.candidateSetId,
},
);
const resultId = rowText(row.result_id);
const decisionReceipt = rowObject(row.decision_receipt);
const revision = rowNumber(row.revision);
const fingerprint = rowText(row.decision_state_fingerprint) ?? input.decisionStateFingerprint;
if (!resultId || !decisionReceipt || revision === null) {
throw new RectificationToolServiceError("invalid_inference_transition");
}
return {
resultId,
revision,
idempotent: row.idempotent === true,
decisionReceipt,
decisionStateFingerprint: fingerprint,
};
}
export function inferenceFingerprintForState(
caseId: string,
evidenceLedgerFingerprint: string,
state: { candidate_set_id: string; revision: number; answered_probes: readonly { probe_id: string }[] },
scoringPolicyVersion = INFERENCE_ALGORITHM_VERSION,
): string {
return decisionStateFingerprint({
caseId,
evidenceLedgerFingerprint,
candidateSetId: state.candidate_set_id,
inferenceRevision: state.revision,
answeredProbeIds: state.answered_probes.map((item) => item.probe_id),
scoringPolicyVersion,
});
}
export type V9AcceptResult = Readonly<{
success: boolean;
savedTime: string;
status: "accepted";
resultId: string;
caseStatus: string;
idempotent: boolean;
}>;
export async function acceptV9Candidate(
accounting: AccountingClient,
userId: string,
caseId: string,
resultId: string,
candidateId: string,
requestId: string,
): Promise<V9AcceptResult> {
if (!uuidPattern.test(candidateId) || !uuidPattern.test(requestId)) {
throw new RectificationToolServiceError("invalid_candidate_ref");
}
const row = await rpc<Record<string, unknown>>(
accounting,
"accept_agentic_rectification_candidate_for_case_v2",
{
p_user_id: userId,
p_case_id: caseId,
p_result_id: resultId,
p_candidate_id: candidateId,
p_request_id: requestId,
},
);
const savedTime = timeValue(row.saved_time);
if (row.success !== true || row.status !== "accepted" || !savedTime) {
throw new RectificationToolServiceError("invalid_accept_result");
}
return {
success: true,
savedTime,
status: "accepted",
resultId: String(row.result_id ?? resultId),
caseStatus: String(row.case_status ?? "candidate_accepted"),
idempotent: row.idempotent === true,
};
}
export type V9ConfirmResult = Readonly<{
success: boolean;
savedTime: string;
status: "confirmed";
resultId: string;
caseStatus: string;
idempotent: boolean;
}>;
export async function confirmV9BirthTime(
accounting: AccountingClient,
userId: string,
caseId: string,
input: {
resultId: string;
candidateId: string;
requestId: string;
consentQuote: string;
sourceTurnId: string;
},
): Promise<V9ConfirmResult> {
if (!uuidPattern.test(input.candidateId) || !uuidPattern.test(input.requestId)) {
throw new RectificationToolServiceError("invalid_candidate_ref");
}
const row = await rpc<Record<string, unknown>>(
accounting,
"confirm_agentic_rectification_candidate_for_case_v2",
{
p_user_id: userId,
p_case_id: caseId,
p_result_id: input.resultId,
p_candidate_id: input.candidateId,
p_request_id: input.requestId,
p_consent_quote: input.consentQuote,
p_source_turn_id: input.sourceTurnId,
},
);
const savedTime = timeValue(row.saved_time);
if (row.success !== true || row.status !== "confirmed" || !savedTime) {
throw new RectificationToolServiceError("invalid_confirm_result");
}
return {
success: true,
savedTime,
status: "confirmed",
resultId: String(row.result_id ?? input.resultId),
caseStatus: String(row.case_status ?? "confirmed"),
idempotent: row.idempotent === true,
};
}
export async function closeV9Case(
accounting: AccountingClient,
userId: string,
caseId: string,
reason: "completed_by_user" | "abandoned_by_user" | "other",
): Promise<Readonly<{ success: boolean; caseId: string; status: string; idempotent: boolean }>> {
const row = await rpc<Record<string, unknown>>(
accounting,
"close_agentic_rectification_case",
{ p_user_id: userId, p_case_id: caseId, p_reason: reason },
);
if (row?.success !== true) throw new RectificationToolServiceError("invalid_close_result");
return {
success: true,
caseId: String(row?.case_id ?? caseId),
status: String(row?.status ?? "closed"),
idempotent: row?.idempotent === true,
};
}
/** Map an RPC error message back to a safe tool error code. */
export function safeToolErrorCode(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
const known = [
"quote_not_grounded",
"consent_not_grounded",
"evidence_not_found",
"evidence_not_confirmable",
"evidence_not_revisable",
"case_terminal",
"case_not_found",
"candidate_not_found",
"candidate_expired",
"candidate_selection_blocked",
"candidate_time_not_allowed",
"candidate_already_selected",
"candidate_superseded",
"candidate_profile_changed",
"confirmation_blocked",
"confirm_time_mismatch",
"case_already_confirmed",
"legacy_skill_identity_unverifiable",
"skill_identity_missing",
"skill_identity_mismatch",
"invalid_skill_identity",
"skill_version_mismatch",
"turn_not_found",
"focus_not_found",
"focus_not_active",
"focus_target_mismatch",
"focus_idempotency_conflict",
"attempt_not_found",
"attempt_already_finalized",
"attempt_not_successful",
"idempotency_conflict",
"invalid_input",
"invalid_choice_copy",
"precision_downgrade",
"offer_not_allowed",
"no_candidate_result",
"stale_probe",
"revision_conflict",
"inference_patch_retired",
];
if (error instanceof RectificationToolServiceError && known.includes(error.code)) {
return error.code;
}
for (const code of known) {
if (message.includes(`agentic_rectification_${code}`)) return code;
}
return "tool_failed";
}
export const V9_EVIDENCE_KINDS = EVIDENCE_KINDS;
export const V9_SKILL_VERSION = RECTIFICATION_SKILL_VERSION;
export type V9PublicTool = PublicRectificationTool;
export type V9PublicPhase = PublicRectificationPhase;