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-016、BUG-114
|
||||||
- 复发自:BUG-114
|
- 复发自:BUG-114
|
||||||
- 修复版本:待本次 staging 修复提交与部署验收
|
- 修复版本:待本次 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 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()) {
|
function currentTimeContext(now = new Date()) {
|
||||||
const chinaTime = new Date(now.getTime() + 8 * 60 * 60 * 1000)
|
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`));
|
controller.enqueue(encoder.encode(`${JSON.stringify(event)}\n`));
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const result = await agent.stream([
|
const result = await agent.stream(
|
||||||
...parsed.data.history.map((message) => message.role === "user"
|
[
|
||||||
? { role: "user" as const, content: message.text }
|
...parsed.data.history.map((message) => message.role === "user"
|
||||||
: { role: "assistant" as const, content: message.text }),
|
? { role: "user" as const, content: message.text }
|
||||||
{
|
: { role: "assistant" as const, content: message.text }),
|
||||||
role: "user",
|
{
|
||||||
content: [
|
role: "user",
|
||||||
currentTimeContext(requestTime),
|
content: [
|
||||||
parsed.data.name ? `用户称呼:${parsed.data.name}` : "",
|
currentTimeContext(requestTime),
|
||||||
parsed.data.action === "opening" ? openingContext : parsed.data.message,
|
parsed.data.name ? `用户称呼:${parsed.data.name}` : "",
|
||||||
].filter(Boolean).join("\n"),
|
parsed.data.action === "opening" ? openingContext : parsed.data.message,
|
||||||
},
|
].filter(Boolean).join("\n"),
|
||||||
]);
|
},
|
||||||
|
],
|
||||||
|
{ maxSteps: agenticRectificationMaxSteps },
|
||||||
|
);
|
||||||
for await (const chunk of result.textStream) {
|
for await (const chunk of result.textStream) {
|
||||||
if (/\S/.test(chunk)) emitted = true;
|
if (/\S/.test(chunk)) emitted = true;
|
||||||
send({ type: "delta", text: chunk });
|
send({ type: "delta", text: chunk });
|
||||||
}
|
}
|
||||||
send({ type: "done", emitted });
|
|
||||||
void recordModelUsage(
|
void recordModelUsage(
|
||||||
accounting,
|
accounting,
|
||||||
userId,
|
userId,
|
||||||
@@ -239,7 +242,15 @@ export async function POST(request: Request) {
|
|||||||
selectedModel.id,
|
selectedModel.id,
|
||||||
result.totalUsage,
|
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();
|
controller.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const reason = error instanceof Error ? error.name : "UnknownError";
|
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\(/);
|
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