From 64a6a5586882fd6082f6d7f574f3391745c6b7a5 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Mon, 3 Aug 2026 23:33:52 +0800 Subject: [PATCH] fix rectification agent empty tool stream --- docs/BUG_HISTORY.md | 7 ++++ .../src/app/api/rectification/agent/route.ts | 41 ++++++++++++------- .../tests/rectification-agentic-entry.test.ts | 7 ++++ 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index e7c313f7..b348e1b3 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -2023,3 +2023,10 @@ - 相关记录:BUG-016、BUG-114 - 复发自:BUG-114 - 修复版本:待本次 staging 修复提交与部署验收 + +## 2026-08-03 — Agentic rectification could finish after tool calls without a visible reply + +- Symptom: `/api/rectification/agent` returned `{"type":"done","emitted":false}` after a user supplied another dated life event. +- Root cause: the Mastra stream used its default step limit. A turn that consumed all available steps on `rectification-*` tool calls ended before the model could produce the final user-facing Chinese response, while the route treated an empty `textStream` as a normal `done` event. +- Fix: allow eight Agent steps so the existing tool workflow can continue from tool results to a final response. Empty streams now return an explicit recoverable error event and remain on the existing refund path instead of silently reporting completion. +- Regression coverage: `frontend/tests/rectification-agentic-entry.test.ts` checks the multi-step stream boundary and rejects the former silent `done` contract. diff --git a/frontend/src/app/api/rectification/agent/route.ts b/frontend/src/app/api/rectification/agent/route.ts index d56b26c2..059338c5 100644 --- a/frontend/src/app/api/rectification/agent/route.ts +++ b/frontend/src/app/api/rectification/agent/route.ts @@ -43,6 +43,7 @@ const agenticRectificationRequestSchema = z.discriminatedUnion("action", [ ]); const openingContext = "The user opened birth-time rectification. Begin the session now: run the required gate, briefly explain the evidence-based process in Simplified Chinese, and ask exactly one natural question about the most useful dated life event. Do not mention this server event."; +const agenticRectificationMaxSteps = 8; function currentTimeContext(now = new Date()) { const chinaTime = new Date(now.getTime() + 8 * 60 * 60 * 1000) @@ -214,24 +215,26 @@ export async function POST(request: Request) { controller.enqueue(encoder.encode(`${JSON.stringify(event)}\n`)); }; try { - const result = await agent.stream([ - ...parsed.data.history.map((message) => message.role === "user" - ? { role: "user" as const, content: message.text } - : { role: "assistant" as const, content: message.text }), - { - role: "user", - content: [ - currentTimeContext(requestTime), - parsed.data.name ? `用户称呼:${parsed.data.name}` : "", - parsed.data.action === "opening" ? openingContext : parsed.data.message, - ].filter(Boolean).join("\n"), - }, - ]); + const result = await agent.stream( + [ + ...parsed.data.history.map((message) => message.role === "user" + ? { role: "user" as const, content: message.text } + : { role: "assistant" as const, content: message.text }), + { + role: "user", + content: [ + currentTimeContext(requestTime), + parsed.data.name ? `用户称呼:${parsed.data.name}` : "", + parsed.data.action === "opening" ? openingContext : parsed.data.message, + ].filter(Boolean).join("\n"), + }, + ], + { maxSteps: agenticRectificationMaxSteps }, + ); for await (const chunk of result.textStream) { if (/\S/.test(chunk)) emitted = true; send({ type: "delta", text: chunk }); } - send({ type: "done", emitted }); void recordModelUsage( accounting, userId, @@ -239,7 +242,15 @@ export async function POST(request: Request) { selectedModel.id, result.totalUsage, ); - await settle(emitted); + if (!emitted) { + console.warn(`[agentic-rectification] empty response request=${requestId}`); + send({ type: "error", message: "生时校正没有生成有效回复,本次不会扣除点数,请重新发送。" }); + await settle(false); + controller.close(); + return; + } + send({ type: "done", emitted: true }); + await settle(true); controller.close(); } catch (error) { const reason = error instanceof Error ? error.name : "UnknownError"; diff --git a/frontend/tests/rectification-agentic-entry.test.ts b/frontend/tests/rectification-agentic-entry.test.ts index ee94769c..acfc5cbc 100644 --- a/frontend/tests/rectification-agentic-entry.test.ts +++ b/frontend/tests/rectification-agentic-entry.test.ts @@ -66,3 +66,10 @@ test("account rehydration normalizes persisted ISO birth dates before completene ); assert.match(profileReader, /const date = normalizePersistedBirthDate\(/); }); + +test("agent tool calls leave a final step for visible prose and never end silently", () => { + assert.match(route, /const agenticRectificationMaxSteps = 8/); + assert.match(route, /\{ maxSteps: agenticRectificationMaxSteps \}/); + assert.match(route, /if \(!emitted\) \{[\s\S]*type: "error"[\s\S]*await settle\(false\)[\s\S]*return;/); + assert.doesNotMatch(route, /send\(\{ type: "done", emitted \}\)/); +});