fix(rectification): restore message actions and retry receipts
Independent Staging Quality Gate / validate (push) Failing after 30m22s
Independent Staging Quality Gate / publish (push) Has been skipped

This commit is contained in:
Jesse_Chen
2026-08-14 00:10:22 +08:00
parent e8952dcc75
commit 2e17ccad64
17 changed files with 1225 additions and 60 deletions
@@ -313,7 +313,7 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
}
await finalize("completed", answerText);
await persistPhase("run.completed", null);
await emit({ type: "run.completed" });
await emit({ type: "run.completed", turnId });
return {
ok: true, turnId, turnStatus: "completed", skillLoaded,
answerText, phases, toolsUsed: [...toolsUsed], errorCode: null,
@@ -0,0 +1,155 @@
import type { MessageListInput } from "@mastra/core/agent/message-list";
import { isTerminalStatus, type RectificationCaseStatus } from "./case-status";
import {
loadV9CaseDossier,
RectificationToolServiceError,
type RectificationRpcClient,
} from "./tool-service";
export type RectificationRegenerationAgent = Readonly<{
getSkill(name: string): Promise<unknown>;
generate(
messages: MessageListInput,
options: { abortSignal?: AbortSignal; maxSteps: number },
): Promise<{ text: string }>;
}>;
export type RegenerateV9AssistantTurnOptions = Readonly<{
userId: string;
caseId: string;
sessionId: string;
turnId: string;
requestId: string;
accounting: RectificationRpcClient;
agent: RectificationRegenerationAgent;
signal?: AbortSignal;
}>;
export type RegenerateV9AssistantTurnResult = Readonly<{
ok: true;
turnId: string;
assistantMessage: string;
idempotent: boolean;
}>;
function latestCompletedAssistantTurn(
turns: Awaited<ReturnType<typeof loadV9CaseDossier>>["turns"],
) {
return [...turns]
.reverse()
.find((turn) => turn.role === "assistant" && turn.status === "completed" && Boolean(turn.text?.trim()));
}
function regenerationPrompt(caseId: string, oldAssistantMessage: string): string {
return [
`当前校正 Case 引用:${caseId}`,
"请先加载绑定 Skill,再调用 rectification-read-case。",
"随后只重写下面这条最近的 Agent 正文,使其更自然、准确,并符合当前服务端事实。不要描述后台过程,不要执行或声称执行任何写操作。",
"",
"待替换的旧正文:",
oldAssistantMessage,
].join("\n");
}
function parseRpcResult(value: unknown): RegenerateV9AssistantTurnResult {
const row = Array.isArray(value) ? value[0] : value;
if (!row || typeof row !== "object") {
throw new RectificationToolServiceError("agentic_rectification_regeneration_invalid_result");
}
const result = row as Record<string, unknown>;
if (
result.ok !== true
|| typeof result.turn_id !== "string"
|| typeof result.assistant_message !== "string"
|| !result.assistant_message.trim()
) {
throw new RectificationToolServiceError("agentic_rectification_regeneration_invalid_result");
}
return {
ok: true,
turnId: result.turn_id,
assistantMessage: result.assistant_message,
idempotent: result.idempotent === true,
};
}
export async function regenerateV9AssistantTurn(
options: RegenerateV9AssistantTurnOptions,
): Promise<RegenerateV9AssistantTurnResult> {
const {
userId,
caseId,
sessionId,
turnId,
requestId,
accounting,
agent,
signal,
} = options;
const { data: existingData, error: existingError } = await accounting.rpc(
"get_agentic_rectification_turn_regeneration",
{
p_user_id: userId,
p_case_id: caseId,
p_turn_id: turnId,
p_request_id: requestId,
},
);
if (existingError) throw new RectificationToolServiceError(existingError.message);
if (existingData) return parseRpcResult(existingData);
const dossier = await loadV9CaseDossier(accounting, userId, caseId);
if (dossier.case.sessionId !== sessionId) {
throw new RectificationToolServiceError("agentic_rectification_case_session_mismatch");
}
if (isTerminalStatus(dossier.case.status as RectificationCaseStatus)) {
throw new RectificationToolServiceError("agentic_rectification_case_terminal");
}
const target = dossier.turns.find((turn) => (
turn.id === turnId
&& turn.role === "assistant"
&& turn.status === "completed"
&& Boolean(turn.text?.trim())
));
if (!target?.text) {
throw new RectificationToolServiceError("agentic_rectification_turn_not_found");
}
const latest = latestCompletedAssistantTurn(dossier.turns);
if (!latest || latest.id !== turnId) {
throw new RectificationToolServiceError("agentic_rectification_turn_not_latest");
}
let skill: unknown = null;
try {
skill = await agent.getSkill(dossier.case.skillName);
} catch {
skill = null;
}
if (!skill) {
throw new RectificationToolServiceError("agentic_rectification_skill_not_loaded");
}
const generated = await agent.generate(
[{ role: "user", content: regenerationPrompt(caseId, target.text) }],
{ abortSignal: signal, maxSteps: 6 },
);
const assistantMessage = generated.text.trim();
if (!assistantMessage) {
throw new RectificationToolServiceError("agentic_rectification_regeneration_empty");
}
const { data, error } = await accounting.rpc(
"regenerate_agentic_rectification_turn",
{
p_user_id: userId,
p_case_id: caseId,
p_session_id: sessionId,
p_turn_id: turnId,
p_request_id: requestId,
p_assistant_message: assistantMessage,
},
);
if (error) throw new RectificationToolServiceError(error.message);
return parseRpcResult(data);
}
@@ -23,6 +23,7 @@ export type PublicPhaseStreamEvent = Readonly<{
text?: string;
tool?: PublicRectificationTool;
methods?: readonly PublicRectificationMethod[];
turnId?: string;
}>;
export type PublicStreamEvent = PublicPhaseStreamEvent | RectificationActivityEvent;
@@ -162,6 +163,7 @@ export function safePublicEvent(value: unknown): PublicStreamEvent | null {
text?: unknown;
tool?: unknown;
methods?: unknown;
turnId?: unknown;
};
if (event.type === "tool.activity") {
if (!isPublicRectificationTool(event.tool) || !isRectificationActivityStatus(event.status)) return null;
@@ -186,10 +188,16 @@ export function safePublicEvent(value: unknown): PublicStreamEvent | null {
const methods = tool && METHOD_TOOLS.has(tool) && Array.isArray(event.methods)
? [...new Set(event.methods.filter(isPublicRectificationMethod))]
: [];
const turnId = type === "run.completed"
&& typeof event.turnId === "string"
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(event.turnId)
? event.turnId
: undefined;
return {
type,
...(text !== undefined ? { text } : {}),
...(tool ? { tool } : {}),
...(methods.length > 0 ? { methods } : {}),
...(turnId ? { turnId } : {}),
};
}