Merge pull request #82 from jesse-ux/codex/rectification-tool-stream-followup
Fix rectification agent empty tool stream
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 \}\)/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user