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
@@ -0,0 +1,76 @@
import type {
PublicRectificationMethod,
PublicRectificationTool,
RectificationActivityStatus,
} from "./rectification-agentic/v9/public-receipt";
export type CompletedActivityReceiptView = Readonly<{
steps: readonly PublicRectificationTool[];
methods: readonly PublicRectificationMethod[];
failedTool?: PublicRectificationTool;
}>;
type ToolTerminalStatus = "completed" | "failed";
export type RectificationActivityReceiptState = Readonly<{
completedSteps: readonly PublicRectificationTool[];
methods: readonly PublicRectificationMethod[];
terminalStatus: Readonly<Partial<Record<PublicRectificationTool, ToolTerminalStatus>>>;
failureOrder: readonly PublicRectificationTool[];
}>;
type ReceiptActivityEvent = Readonly<{
tool: PublicRectificationTool;
status: RectificationActivityStatus;
methods?: readonly PublicRectificationMethod[];
}>;
export function createRectificationActivityReceiptState(): RectificationActivityReceiptState {
return {
completedSteps: [],
methods: [],
terminalStatus: {},
failureOrder: [],
};
}
export function reduceRectificationActivityReceipt(
state: RectificationActivityReceiptState,
event: ReceiptActivityEvent,
): RectificationActivityReceiptState {
if (event.status === "started") return state;
const terminalStatus = { ...state.terminalStatus, [event.tool]: event.status };
if (event.status === "completed") {
return {
completedSteps: state.completedSteps.includes(event.tool)
? state.completedSteps
: [...state.completedSteps, event.tool],
methods: [...new Set([...state.methods, ...(event.methods ?? [])])],
terminalStatus,
failureOrder: state.failureOrder.filter((tool) => tool !== event.tool),
};
}
return {
...state,
terminalStatus,
failureOrder: [
...state.failureOrder.filter((tool) => tool !== event.tool),
event.tool,
],
};
}
export function receiptFromRectificationActivityState(
state: RectificationActivityReceiptState,
): CompletedActivityReceiptView {
const failedTool = [...state.failureOrder]
.reverse()
.find((tool) => state.terminalStatus[tool] === "failed");
return {
steps: [...state.completedSteps],
methods: [...state.methods],
...(failedTool ? { failedTool } : {}),
};
}
@@ -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 } : {}),
};
}