feat: persist rectification analysis traces

This commit is contained in:
Jesse_Chen
2026-07-30 03:15:06 +08:00
parent 29374ded2a
commit ee10fb4d8a
19 changed files with 1084 additions and 32 deletions
@@ -26,15 +26,19 @@ export function createRectificationV4CaseService(
const realizeQuestion = options.regenerateQuestion ?? regenerateQuestionRealization;
async function response(userId: string, caseValue: RectificationV4Case, jobId?: string): Promise<RectificationV4ApiResponse> {
const [events, turns] = await Promise.all([
const [events, turns, analysis] = await Promise.all([
store.loadEvents(userId, caseValue.id),
store.loadTurns(userId, caseValue.id),
caseValue.deploymentMode === "v5_agent"
? store.loadAnalysisMessages(userId, caseValue.id)
: Promise.resolve([]),
]);
return {
case: caseValue,
job: jobId ? await store.loadJob(userId, jobId) : null,
events: [...events],
turns: [...turns],
analysis: [...analysis],
};
}
@@ -270,11 +270,49 @@ export const rectificationV4JobSchema = z.object({
}).strict();
export type RectificationV4Job = z.infer<typeof rectificationV4JobSchema>;
export const rectificationAnalysisStageSchema = z.object({
phase: z.enum([
"extracting_evidence",
"scoring_candidates",
"checking_robustness",
"planning_question",
"reasoning",
"rendering",
]),
label: z.string().trim().min(1).max(120),
status: z.enum(["completed", "failed"]),
durationMs: z.number().int().min(0).max(300_000).nullable(),
}).strict();
export const rectificationAnalysisToolCallSchema = z.object({
category: z.enum(["candidate_engine", "diagnostic", "agent_diagnostic"]),
label: z.string().trim().min(1).max(120),
outcome: z.enum(["succeeded", "failed", "rejected"]),
durationMs: z.number().int().min(0).max(300_000).nullable(),
}).strict();
export const rectificationAnalysisTraceSchema = z.object({
status: z.enum(["completed", "failed", "legacy"]),
stages: z.array(rectificationAnalysisStageSchema).max(12),
toolCalls: z.array(rectificationAnalysisToolCallSchema).max(16),
techniques: z.array(z.string().trim().min(1).max(120)).max(24),
reasoningSummary: z.string().trim().min(1).max(500).nullable(),
reasoningSource: z.enum(["provider_summary", "none"]),
}).strict();
export type RectificationAnalysisTrace = z.infer<typeof rectificationAnalysisTraceSchema>;
export const rectificationAnalysisItemSchema = z.object({
sourceTurnId: z.string().uuid(),
trace: rectificationAnalysisTraceSchema,
}).strict();
export type RectificationAnalysisItem = z.infer<typeof rectificationAnalysisItemSchema>;
export const rectificationV4ApiResponseSchema = z.object({
case: rectificationV4CaseSchema,
job: rectificationV4JobSchema.nullable(),
events: z.array(lifeEventRevisionSchema),
turns: z.array(rectificationV4TurnSchema),
analysis: z.array(rectificationAnalysisItemSchema).optional(),
}).strict();
export type RectificationV4ApiResponse = z.infer<typeof rectificationV4ApiResponseSchema>;
@@ -1,4 +1,4 @@
import type { AgentRun, CandidateFeatureSnapshot, DiagnosticsSummary, PublicMessage, ValidatedDecision } from "../rectification-agent/contracts.ts";
import type { AgentRun, CandidateFeatureSnapshot, DiagnosticsSummary, StoredPublicMessage, ValidatedDecision } from "../rectification-agent/contracts.ts";
import type {
LifeEventRevision,
PendingEvidence,
@@ -20,7 +20,7 @@ export function createRectificationV4MemoryStore(): RectificationV4Store & {
readonly diagnostics: Map<string, DiagnosticsSummary>;
readonly featureSnapshots: Map<string, CandidateFeatureSnapshot>;
readonly agentRuns: Map<string, AgentRun>;
readonly publicMessages: Map<string, PublicMessage>;
readonly publicMessages: Map<string, StoredPublicMessage>;
readonly validatedDecisions: Map<string, ValidatedDecision>;
readonly pendingEvidence: Map<string, PendingEvidence>;
} {
@@ -32,7 +32,7 @@ export function createRectificationV4MemoryStore(): RectificationV4Store & {
const diagnostics = new Map<string, DiagnosticsSummary>();
const featureSnapshots = new Map<string, CandidateFeatureSnapshot>();
const agentRuns = new Map<string, AgentRun>();
const publicMessages = new Map<string, PublicMessage>();
const publicMessages = new Map<string, StoredPublicMessage>();
const validatedDecisions = new Map<string, ValidatedDecision>();
const pendingEvidence = new Map<string, PendingEvidence>();
@@ -69,6 +69,13 @@ export function createRectificationV4MemoryStore(): RectificationV4Store & {
.filter((turn) => turn.caseId === caseId)
.sort((left, right) => left.caseVersion - right.caseVersion || left.createdAt.localeCompare(right.createdAt));
},
async loadAnalysisMessages(userId, caseId) {
owned(userId, caseId);
return [...jobs.values()]
.filter((job) => job.caseId === caseId && publicMessages.get(job.id)?.analysisTrace)
.sort((left, right) => turns.get(left.turnId)!.caseVersion - turns.get(right.turnId)!.caseVersion)
.map((job) => ({ sourceTurnId: job.turnId, trace: publicMessages.get(job.id)!.analysisTrace! }));
},
async loadLatestValidatedDecision(userId, caseId) {
const caseValue = cases.get(caseId);
if (!caseValue || caseValue.userId !== userId) return null;
+4 -2
View File
@@ -1,8 +1,9 @@
import type { AgentRun, CandidateFeatureSnapshot, DiagnosticsSummary, PublicMessage, ValidatedDecision } from "../rectification-agent/contracts.ts";
import type { AgentRun, CandidateFeatureSnapshot, DiagnosticsSummary, StoredPublicMessage, ValidatedDecision } from "../rectification-agent/contracts.ts";
import type {
CandidateSnapshot,
LifeEventRevision,
PendingEvidence,
RectificationAnalysisItem,
RectificationV4Case,
RectificationV4Job,
RectificationV4Phase,
@@ -33,7 +34,7 @@ export type CompleteRectificationV4JobInput = Readonly<{
diagnostics: DiagnosticsSummary | null;
featureSnapshot: CandidateFeatureSnapshot | null;
validatedDecision: ValidatedDecision;
publicMessage: PublicMessage;
publicMessage: StoredPublicMessage;
agentRun: AgentRun;
nextQuestion: RectificationV4Question | null;
status: RectificationV4Case["status"];
@@ -45,6 +46,7 @@ export interface RectificationV4Store {
loadCase(userId: string, caseId: string): Promise<RectificationV4Case | null>;
loadEvents(userId: string, caseId: string): Promise<readonly LifeEventRevision[]>;
loadTurns(userId: string, caseId: string): Promise<readonly RectificationV4Turn[]>;
loadAnalysisMessages(userId: string, caseId: string): Promise<readonly RectificationAnalysisItem[]>;
loadLatestValidatedDecision(userId: string, caseId: string): Promise<ValidatedDecision | null>;
loadActionCase(userId: string, actionId: string): Promise<RectificationV4Case | null>;
createCase(input: { readonly case: RectificationV4Case; readonly actionId: string }): Promise<RectificationV4Case>;
@@ -1,13 +1,15 @@
import type { SupabaseClient } from "@supabase/supabase-js";
import { validatedDecisionSchema, type ValidatedDecision } from "../rectification-agent/contracts.ts";
import { storedPublicMessageSchema, validatedDecisionSchema, type ValidatedDecision } from "../rectification-agent/contracts.ts";
import {
candidateSnapshotSchema,
lifeEventRevisionSchema,
rectificationAnalysisItemSchema,
rectificationV4CaseSchema,
rectificationV4JobSchema,
rectificationV4TurnSchema,
type CandidateSnapshot,
type LifeEventRevision,
type RectificationAnalysisItem,
type RectificationV4Case,
type RectificationV4Job,
type RectificationV4Turn,
@@ -22,6 +24,24 @@ import { evidenceSetHash, rectificationFingerprint } from "./fingerprints.ts";
type Row = Record<string, unknown>;
export function projectAnalysisMessages(
publicMessageRows: readonly Readonly<Row>[],
jobRows: readonly Readonly<Row>[],
): readonly RectificationAnalysisItem[] {
const turnByJob = new Map(jobRows.map((row) => [String(row.id), row.turn_id]));
return [...publicMessageRows]
.sort((left, right) => timestamp(left.created_at).localeCompare(timestamp(right.created_at)))
.flatMap((row) => {
const message = storedPublicMessageSchema.safeParse(row.message);
if (!message.success || !message.data.analysisTrace) return [];
const item = rectificationAnalysisItemSchema.safeParse({
sourceTurnId: turnByJob.get(String(row.job_id)),
trace: message.data.analysisTrace,
});
return item.success ? [item.data] : [];
});
}
function timestamp(value: unknown): string {
return value instanceof Date ? value.toISOString() : String(value);
}
@@ -183,6 +203,21 @@ export function createRectificationV4SupabaseStore(supabase: SupabaseClient): Re
return ((data ?? []) as Row[]).map(turnValue);
}
async function loadAnalysisMessagesByCase(userId: string, caseId: string): Promise<readonly RectificationAnalysisItem[]> {
if (!await loadCaseById(userId, caseId)) throw new RectificationV4StoreError("not_found");
const { data, error } = await supabase.from("birth_time_rectification_public_messages")
.select("job_id,message,created_at").eq("case_id", caseId).eq("user_id", userId)
.order("created_at", { ascending: true });
if (error) throw storeError(error);
const rows = (data ?? []) as Row[];
if (rows.length === 0) return [];
const jobIds = rows.map((row) => String(row.job_id));
const { data: jobData, error: jobError } = await supabase.from("birth_time_rectification_v4_jobs")
.select("id,turn_id").eq("case_id", caseId).eq("user_id", userId).in("id", jobIds);
if (jobError) throw storeError(jobError);
return projectAnalysisMessages(rows, (jobData ?? []) as Row[]);
}
async function rpc(name: string, args: Row): Promise<unknown> {
const { data, error } = await supabase.rpc(name, args);
if (error) throw storeError(error);
@@ -200,6 +235,7 @@ export function createRectificationV4SupabaseStore(supabase: SupabaseClient): Re
loadCase: loadCaseById,
loadEvents: loadEventsByCase,
loadTurns: loadTurnsByCase,
loadAnalysisMessages: loadAnalysisMessagesByCase,
async loadLatestValidatedDecision(userId, caseId): Promise<ValidatedDecision | null> {
const { data, error } = await supabase.from("birth_time_rectification_agent_runs")
.select("validated_decision_json").eq("case_id", caseId).eq("user_id", userId)