Skip to content

Commit 712539c

Browse files
ericallamclaude
andcommitted
fix(sdk): keep late response parts on a skipped empty response
When the model returns no content the response is no longer accumulated, but a non-transient data part written during onBeforeTurnComplete still has to be persisted. The late-part drain now commits the previously skipped response into the UI transcript (kept out of the model lane, since the parts are UI-only) instead of clearing the parts without saving them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VG39FXXkFFU24U5EtJMwPi
1 parent fd27a6c commit 712539c

2 files changed

Lines changed: 40 additions & 3 deletions

File tree

packages/trigger-sdk/src/v3/ai.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9482,10 +9482,18 @@ function chatAgent<
94829482
parts: [...(msg.parts ?? []), ...lateParts],
94839483
} as TUIMessage;
94849484
capturedResponseMessage = accumulatedUIMessages[idx] as TUIMessage;
9485-
capturedPartialResponse = capturedResponseMessage;
9486-
turnCompleteEvent.responseMessage = capturedResponseMessage;
9487-
turnCompleteEvent.uiMessages = accumulatedUIMessages;
9485+
} else {
9486+
capturedResponseMessage = {
9487+
...capturedResponseMessage,
9488+
parts: [...(capturedResponseMessage.parts ?? []), ...lateParts],
9489+
} as TUIMessage;
9490+
accumulatedUIMessages.push(capturedResponseMessage);
9491+
turnNewUIMessages.push(capturedResponseMessage);
94889492
}
9493+
capturedPartialResponse = capturedResponseMessage;
9494+
turnCompleteEvent.responseMessage = capturedResponseMessage;
9495+
turnCompleteEvent.uiMessages = accumulatedUIMessages;
9496+
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
94899497
locals.set(chatResponsePartsKey, []);
94909498
}
94919499

packages/trigger-sdk/test/transcript-changesets.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,35 @@ describe("chat.agent transcript changesets", () => {
528528
await harness.close();
529529
}
530530
});
531+
532+
it("persists a late response part written by onBeforeTurnComplete on an empty response", async () => {
533+
const chatId = "changeset-late-part";
534+
const model = new MockLanguageModelV3({
535+
doStream: async () => ({
536+
stream: simulateReadableStream({
537+
chunks: [{ type: "finish", finishReason: { unified: "stop", raw: "stop" }, usage }],
538+
}),
539+
}),
540+
});
541+
const agent = chat.agent({
542+
id: "changeset-late-part",
543+
onBeforeTurnComplete: async ({ writer }) => {
544+
writer.write({ type: "data-note", data: { text: "late note" } } as never);
545+
},
546+
run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }),
547+
});
548+
const harness = mockChatAgent(agent, { chatId });
549+
try {
550+
await harness.sendMessage(userMessage("hello", "u1"));
551+
await waitFor(() => storage.changesets.length === 1, "turn save");
552+
const entries = storage.transcript(chatId)!.entries;
553+
expect(entries.map((e) => e.message.role)).toEqual(["user", "assistant"]);
554+
const assistant = entries.find((e) => e.message.role === "assistant");
555+
expect(JSON.stringify(assistant?.message)).toContain("late note");
556+
} finally {
557+
await harness.close();
558+
}
559+
});
531560
});
532561

533562
function assistantMessage(id: string): UIMessage {

0 commit comments

Comments
 (0)