fix: restore rectification agent message actions

This commit is contained in:
Jesse_Chen
2026-07-30 00:41:48 +08:00
parent a5f409a500
commit 0a5b8cd239
15 changed files with 513 additions and 4 deletions
@@ -1,5 +1,6 @@
import path from "node:path";
import { Agent } from "@mastra/core/agent";
import { z } from "zod";
import { defaultLanguageModel, resolveLanguageModel } from "@/mastra/model";
import type { CandidateSnapshot, LifeEventRevision, PendingEvidence, RectificationV4Case } from "../rectification-v4/contracts.ts";
import { publicMessageSchema, type PublicMessage, type QuestionOpportunity, type ValidatedDecision } from "./contracts.ts";
@@ -14,6 +15,7 @@ const multiQuestionMoves = /(?:另外|还有|同时再说|并且告诉我|顺便
const cannedQuestion = /(?:承接[“\"']?.{0,80}[”\"']??请再说一件|接下来请继续讲另一件|我会顺着你的叙述继续核对)/;
const exactClockMinute = /(?:[01]?\d|2[0-3])[:][0-5]\d|(?:[零〇一二两三四五六七八九十]{1,3}|(?:[01]?\d|2[0-3]))点(?:[零〇一二两三四五六七八九十]{1,3}|[0-5]?\d)分/;
const exactMinuteClaim = /(?:唯一|准确|精确|确切|确认|确定|代表).{0,12}(?:出生|生时)?(?:时间|时刻|分钟)|(?:出生|生时)(?:时间|时刻|分钟)?.{0,12}(?:唯一|准确|精确|确切|确认|确定|代表|就是)/;
const questionRealizationSchema = z.object({ question: z.string().trim().min(1).max(1_000) }).strict();
function agentFor(modelId: string | null): { id: string; agent: Agent } | null {
const selected = (modelId ? resolveLanguageModel(modelId) : null) ?? defaultLanguageModel();
@@ -199,3 +201,42 @@ export async function renderPublicTurn(input: Readonly<{
return fallback;
}
}
export async function regenerateQuestionRealization(input: Readonly<{
caseValue: RectificationV4Case;
currentPrompt: string;
latestAnswer: string;
acceptedEvents: readonly LifeEventRevision[];
opportunity: QuestionOpportunity;
timeoutMs?: number;
}>): Promise<string> {
const selected = agentFor(input.caseValue.narrationModelId);
if (!selected) return input.currentPrompt;
try {
const result = await selected.agent.generate(JSON.stringify({
task: "Rewrite the current question naturally without changing its semantic target. Return one question only.",
currentPrompt: input.currentPrompt,
latestAnswer: input.latestAnswer,
recentEvents: input.acceptedEvents.slice(-5).map((event) => ({
summary: event.summary,
date: event.dateRange.label,
subject: event.subject,
})),
selectedOpportunity: {
kind: input.opportunity.kind,
goal: input.opportunity.goal,
requestedFields: input.opportunity.requestedFields,
anchors: input.opportunity.anchors,
contextFacts: input.opportunity.contextFacts,
forbiddenMoves: input.opportunity.forbiddenMoves,
},
}), {
abortSignal: AbortSignal.timeout(input.timeoutMs ?? 15_000),
structuredOutput: { schema: questionRealizationSchema, jsonPromptInjection: "inline" },
});
const question = questionRealizationSchema.parse(result.object).question;
return validateQuestionRealization(question, input.opportunity).valid ? question : input.currentPrompt;
} catch {
return input.currentPrompt;
}
}
@@ -8,6 +8,7 @@ import type {
import { rectificationAgentV5Protocol, rectificationV4AlgorithmVersion, rectificationV4Protocol } from "./contracts.ts";
import { selectRectificationDeploymentMode } from "../rectification-agent/feature-policy.ts";
import { CURRENT_RECTIFICATION_PROMPT_VERSION, CURRENT_RECTIFICATION_SKILL_VERSION } from "../rectification-agent/contracts.ts";
import { regenerateQuestionRealization } from "../rectification-agent/renderer-agent.ts";
import { calculationSpecHash, evidenceSetHash } from "./fingerprints.ts";
import { openingQuestion } from "./opening-question.ts";
import type { RectificationV4Store } from "./store.ts";
@@ -88,6 +89,40 @@ export function createRectificationV4CaseService(store: RectificationV4Store, op
return response(input.userId, saved.case, saved.job.id);
},
async regenerateQuestion(input: {
readonly userId: string;
readonly caseId: string;
readonly actionId: string;
readonly expectedCaseVersion: number;
}) {
const current = await store.loadCase(input.userId, input.caseId);
if (!current?.currentQuestion || current.deploymentMode !== "v5_agent") return null;
const validated = await store.loadLatestValidatedDecision(input.userId, input.caseId);
const opportunity = validated?.selectedOpportunity;
if (!opportunity) return null;
const [events, turns] = await Promise.all([
store.loadEvents(input.userId, input.caseId),
store.loadTurns(input.userId, input.caseId),
]);
const prompt = await regenerateQuestionRealization({
caseValue: current,
currentPrompt: current.currentQuestion.prompt,
latestAnswer: turns.at(-1)?.answer ?? "",
acceptedEvents: events,
opportunity,
});
const nextQuestion = {
...current.currentQuestion,
id: randomUUID(),
prompt,
};
return response(input.userId, await store.replaceCurrentQuestion({
...input,
question: nextQuestion,
now: now().toISOString(),
}));
},
async reviseEvent(input: {
readonly userId: string;
readonly caseId: string;
@@ -63,6 +63,12 @@ export function answerRectificationV4(caseId: string, expectedCaseVersion: numbe
});
}
export function regenerateRectificationV4Question(caseId: string, expectedCaseVersion: number) {
return post(`/api/rectification/v4/cases/${caseId}/regenerate`, {
actionId: globalThis.crypto.randomUUID(), expectedCaseVersion,
});
}
export function transitionRectificationV4(
caseId: string,
expectedCaseVersion: number,
@@ -69,6 +69,14 @@ export function createRectificationV4MemoryStore(): RectificationV4Store & {
.filter((turn) => turn.caseId === caseId)
.sort((left, right) => left.caseVersion - right.caseVersion || left.createdAt.localeCompare(right.createdAt));
},
async loadLatestValidatedDecision(userId, caseId) {
const caseValue = cases.get(caseId);
if (!caseValue || caseValue.userId !== userId) return null;
const latest = [...agentRuns.values()]
.filter((run) => run.caseId === caseId)
.sort((left, right) => right.caseVersion - left.caseVersion || right.createdAt.localeCompare(left.createdAt))[0];
return latest ? validatedDecisions.get(latest.jobId) ?? latest.validatedDecision : null;
},
async createCase(input) {
const replay = actionResults.get(`${input.case.userId}:${input.actionId}`);
if (replay) return owned(input.case.userId, replay.caseId);
@@ -91,6 +99,29 @@ export function createRectificationV4MemoryStore(): RectificationV4Store & {
actionResults.set(`${input.case.userId}:${input.actionId}`, { caseId: input.case.id, jobId: null });
return input.case;
},
async replaceCurrentQuestion(input) {
const key = `${input.userId}:${input.actionId}`;
const replay = actionResults.get(key);
if (replay) return owned(input.userId, replay.caseId);
const current = owned(input.userId, input.caseId);
if (current.version !== input.expectedCaseVersion) throw new RectificationV4StoreError("stale_version");
if (current.deploymentMode !== "v5_agent"
|| !["awaiting_answer", "range_ready"].includes(current.status)
|| !current.currentQuestion) throw new RectificationV4StoreError("invalid_state");
const updated: RectificationV4Case = {
...current,
version: current.version + 1,
currentQuestion: {
...input.question,
domain: current.currentQuestion.domain,
targetEventId: current.currentQuestion.targetEventId,
},
updatedAt: input.now,
};
cases.set(current.id, updated);
actionResults.set(key, { caseId: current.id, jobId: null });
return updated;
},
async submitAnswer(input) {
const key = `${input.userId}:${input.actionId}`;
const replay = actionResults.get(key);
@@ -45,7 +45,16 @@ 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[]>;
loadLatestValidatedDecision(userId: string, caseId: string): Promise<ValidatedDecision | null>;
createCase(input: { readonly case: RectificationV4Case; readonly actionId: string }): Promise<RectificationV4Case>;
replaceCurrentQuestion(input: {
readonly userId: string;
readonly caseId: string;
readonly actionId: string;
readonly expectedCaseVersion: number;
readonly question: RectificationV4Question;
readonly now: string;
}): Promise<RectificationV4Case>;
submitAnswer(input: {
readonly userId: string;
readonly caseId: string;
@@ -1,4 +1,5 @@
import type { SupabaseClient } from "@supabase/supabase-js";
import { validatedDecisionSchema, type ValidatedDecision } from "../rectification-agent/contracts.ts";
import {
candidateSnapshotSchema,
lifeEventRevisionSchema,
@@ -199,6 +200,14 @@ export function createRectificationV4SupabaseStore(supabase: SupabaseClient): Re
loadCase: loadCaseById,
loadEvents: loadEventsByCase,
loadTurns: loadTurnsByCase,
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)
.order("case_version", { ascending: false }).order("created_at", { ascending: false })
.limit(1).maybeSingle();
if (error) throw storeError(error);
return data ? validatedDecisionSchema.parse((data as Row).validated_decision_json) : null;
},
async createCase(input) {
const id = String(await rpc("create_birth_time_rectification_v5_case", {
p_user_id: input.case.userId,
@@ -222,6 +231,19 @@ export function createRectificationV4SupabaseStore(supabase: SupabaseClient): Re
if (!value) throw new RectificationV4StoreError("not_found");
return value;
},
async replaceCurrentQuestion(input) {
const id = String(await rpc("replace_birth_time_rectification_v4_current_question", {
p_user_id: input.userId,
p_case_id: input.caseId,
p_action_id: input.actionId,
p_expected_version: input.expectedCaseVersion,
p_question: input.question,
p_now: input.now,
}));
const value = await loadCaseById(input.userId, id);
if (!value) throw new RectificationV4StoreError("not_found");
return value;
},
async submitAnswer(input) {
const jobId = String(await rpc("submit_birth_time_rectification_v4_answer", {
p_user_id: input.userId,