Просмотр исходного кода

fix: keep failed agent turns uncommitted

fanghanjun 6 месяцев назад
Родитель
Сommit
fb026b9f6a

+ 19 - 2
packages/core/src/__tests__/agent-session.test.ts

@@ -130,6 +130,7 @@ vi.mock("@mariozechner/pi-ai", async () => {
 
 import { runAgentSession, evictAgentCache } from "../agent/agent-session.js";
 import { appendTranscriptEvent, readTranscriptEvents } from "../interaction/session-transcript.js";
+import { restoreAgentMessagesFromTranscript } from "../interaction/session-transcript-restore.js";
 
 describe("runAgentSession cache — bookId switch", () => {
   let projectRoot: string;
@@ -152,6 +153,7 @@ describe("runAgentSession cache — bookId switch", () => {
 
   afterEach(async () => {
     evictAgentCache("s1");
+    evictAgentCache("s-error");
     await rm(projectRoot, { recursive: true, force: true });
   });
 
@@ -599,16 +601,31 @@ describe("runAgentSession cache — bookId switch", () => {
     expect(body).toContain("资料");
   });
 
-  it("返回 pi-agent-core final assistant error,供 API 层保留真实根因", async () => {
+  it("final assistant error writes request_failed instead of request_committed", async () => {
     const model = { provider: "x", id: "y", api: "anthropic-messages" } as any;
     const pipeline = {} as any;
 
     const result = await runAgentSession(
-      { sessionId: "s1", bookId: "book-a", language: "zh", pipeline, projectRoot, model },
+      { sessionId: "s-error", bookId: "book-a", language: "zh", pipeline, projectRoot, model },
       "model error",
     );
 
     expect(result.responseText).toBe("");
     expect(result.errorMessage).toBe("400 status code (no body)");
+
+    const events = await readTranscriptEvents(projectRoot, "s-error");
+    expect(events.map((event) => event.type)).toContain("request_failed");
+    expect(events.map((event) => event.type)).not.toContain("request_committed");
+
+    const restored = await restoreAgentMessagesFromTranscript(projectRoot, "s-error");
+    expect(restored).toEqual([]);
+
+    const instancesAfterError = agentInstances.length;
+    await runAgentSession(
+      { sessionId: "s-error", bookId: "book-a", language: "zh", pipeline, projectRoot, model },
+      "again",
+    );
+    expect(agentInstances).toHaveLength(instancesAfterError + 1);
+    expect(JSON.stringify(streamCalls.at(-1)?.context.messages)).not.toContain("model error");
   });
 });

+ 48 - 22
packages/core/src/agent/agent-session.ts

@@ -227,6 +227,24 @@ function extractTextFromAssistant(msg: AssistantMessage): string {
     .join("");
 }
 
+function lastAssistantMessage(messages: AgentMessage[]): AssistantMessage | undefined {
+  for (let i = messages.length - 1; i >= 0; i--) {
+    const msg = messages[i];
+    if (msg && typeof msg === "object" && "role" in msg && (msg as { role?: unknown }).role === "assistant") {
+      return msg as AssistantMessage;
+    }
+  }
+  return undefined;
+}
+
+function assistantErrorMessage(message: AssistantMessage | undefined): string | undefined {
+  return message &&
+    (message.stopReason === "error" || message.stopReason === "aborted") &&
+    message.errorMessage
+      ? message.errorMessage
+      : undefined;
+}
+
 function convertAgentMessagesForModel(messages: AgentMessage[], model: Model<Api>): Message[] {
   const llmMessages = messages.filter((message): message is Message => {
     if (!message || typeof message !== "object" || !("role" in message)) return false;
@@ -552,17 +570,36 @@ export async function runAgentSession(
   });
 
   // ----- Execute the turn -----
+  let finalAssistant: AssistantMessage | undefined;
+  let errorMessage: string | undefined;
+
   try {
     await agent.prompt(userMessage);
-    await appendTranscriptEvent(projectRoot, {
-      type: "request_committed",
-      version: 1,
-      sessionId,
-      requestId,
-      seq: seq++,
-      timestamp: Date.now(),
-    });
-    cached.lastCommittedSeq = seq - 1;
+
+    finalAssistant = lastAssistantMessage(agent.state.messages);
+    errorMessage = assistantErrorMessage(finalAssistant);
+    if (errorMessage) {
+      await appendTranscriptEvent(projectRoot, {
+        type: "request_failed",
+        version: 1,
+        sessionId,
+        requestId,
+        seq: seq++,
+        timestamp: Date.now(),
+        error: errorMessage,
+      });
+      agentCache.delete(sessionId);
+    } else {
+      await appendTranscriptEvent(projectRoot, {
+        type: "request_committed",
+        version: 1,
+        sessionId,
+        requestId,
+        seq: seq++,
+        timestamp: Date.now(),
+      });
+      cached.lastCommittedSeq = seq - 1;
+    }
   } catch (error) {
     await appendTranscriptEvent(projectRoot, {
       type: "request_failed",
@@ -580,20 +617,9 @@ export async function runAgentSession(
 
   // ----- Extract result -----
   const allMessages = agent.state.messages;
-  let finalAssistant: AssistantMessage | undefined;
-  for (let i = allMessages.length - 1; i >= 0; i--) {
-    const msg = allMessages[i];
-    if (msg && typeof msg === "object" && "role" in msg && (msg as any).role === "assistant") {
-      finalAssistant = msg as AssistantMessage;
-      break;
-    }
-  }
+  finalAssistant ??= lastAssistantMessage(allMessages);
   const responseText = finalAssistant ? extractTextFromAssistant(finalAssistant) : "";
-  const errorMessage = finalAssistant &&
-    (finalAssistant.stopReason === "error" || finalAssistant.stopReason === "aborted") &&
-    finalAssistant.errorMessage
-      ? finalAssistant.errorMessage
-      : undefined;
+  errorMessage ??= assistantErrorMessage(finalAssistant);
 
   return {
     responseText,