fix(rectification): restore message actions and retry receipts
This commit is contained in:
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user