Skip to content

Commit 65fc6d2

Browse files
committed
chore(sdk): strip explanatory comments and trim the changeset
Address review feedback: remove the added code comments (the repo keeps the why in the PR/commit, not the source) and shorten the changeset to a couple of user-facing sentences.
1 parent 0316b05 commit 65fc6d2

3 files changed

Lines changed: 1 addition & 48 deletions

File tree

.changeset/chat-agent-preserve-partial-on-stream-failure.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@trigger.dev/sdk": patch
33
---
44

5-
Preserve the partial assistant message when a chat turn's model stream fails mid-response (e.g. a transport timeout). Previously the streamed-so-far output was dropped: `chat.agent`'s `onTurnComplete` fired with `responseMessage: undefined`, and `chat.createSession`'s `turn.complete()` rethrew without keeping the partial. Now the recovered partial is passed to `onTurnComplete` (on `responseMessage`, `uiMessages`, and `newUIMessages`) and accumulated before `turn.complete()` rethrows, so it survives for persistence even when `hydrateMessages` disables boot-time replay recovery. The turn is still reported as errored.
5+
Preserve the partial assistant message when a chat turn's model stream fails mid-response. `chat.agent` now passes the recovered partial to `onTurnComplete`, and `chat.createSession`'s `turn.complete()` keeps it before rethrowing, instead of dropping the streamed-so-far output.

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

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6389,12 +6389,6 @@ function chatAgent<
63896389
// Declared here so the finally can detach it — a handler leaked past
63906390
// its turn duplicates every mid-stream message into the shared buffer.
63916391
let turnMsgSub: { off: () => void } | undefined;
6392-
// Declared at turn scope (not inside the span callback) so the error
6393-
// handler below can recover the partial assistant output when a
6394-
// source-stream failure abandons the turn before onTurnComplete's
6395-
// success-path capture runs. `capturedPartialResponse` holds the
6396-
// onFinish message if it fired; otherwise the buffered chunks are
6397-
// reconstructed as the fallback (mirrors chat.pipeAndCapture).
63986392
let capturedPartialResponse: TUIMessage | undefined;
63996393
let responseCommitted = false;
64006394
const turnBufferedChunks: UIMessageChunk[] = [];
@@ -7189,11 +7183,6 @@ function chatAgent<
71897183
resolveOnFinish!();
71907184
},
71917185
});
7192-
// Buffer chunks as they flow to the pipe so a source-stream
7193-
// transport failure — which abandons the UI stream before
7194-
// onFinish fires — can still reconstruct the partial in the
7195-
// error handler instead of dropping it. The happy path pays
7196-
// nothing extra; reassembly only runs in the error fallback.
71977186
await pipeChat(tapUIMessageChunks(uiStream, turnBufferedChunks), {
71987187
signal: combinedSignal,
71997188
spanName: "stream response",
@@ -7926,26 +7915,13 @@ function chatAgent<
79267915
? [...accumulatedUIMessages, erroredWireMessage]
79277916
: accumulatedUIMessages;
79287917

7929-
// Recover the partial assistant output the model streamed before the
7930-
// failure. A source-stream transport error (e.g. UND_ERR_BODY_TIMEOUT)
7931-
// abandons the UI stream before onFinish fires, so fall back to
7932-
// reconstructing the partial from the buffered chunks. Surfacing it on
7933-
// the error-path event lets persistence keep the partial instead of
7934-
// losing it — critical when hydrateMessages disables boot-time tail
7935-
// replay, so the recovery path can't reclaim it later. Empty for
7936-
// non-stream failures (nothing buffered), preserving prior behavior.
79377918
let partialResponse: TUIMessage | undefined =
79387919
capturedPartialResponse ??
79397920
((await assemblePartialFromChunks(turnBufferedChunks)) as TUIMessage | undefined);
79407921
if (partialResponse) {
79417922
partialResponse = cleanupAbortedParts(partialResponse);
79427923
}
79437924

7944-
// Build the complete error UI state. A HITL/tool continuation partial
7945-
// reuses an existing assistant id, so replace it in place (like the
7946-
// success path) instead of dropping it as a dup; otherwise append.
7947-
// `erroredWireMessage` was already folded into `erroredUIMessages`
7948-
// above when the pre-run merge hadn't happened.
79497925
let partialIdx = partialResponse?.id
79507926
? erroredUIMessages.findIndex((m) => m.id === partialResponse!.id)
79517927
: -1;
@@ -7982,16 +7958,6 @@ function chatAgent<
79827958

79837959
let erroredNewModelMessages: ModelMessage[] = [];
79847960

7985-
// Commit the complete error state to the canonical accumulator so the
7986-
// errored user message and any recovered partial survive past this
7987-
// hook: the run stays alive after an error, so the next turn sees
7988-
// them, and the error-path snapshot below (the recovery source for
7989-
// non-hydrate apps) persists them for reboot. Matches the success
7990-
// path. Skip when nothing changed so an errored turn never needlessly
7991-
// reconverts. When the only change is an appended partial, push just
7992-
// its model messages to preserve a prior turn's compaction (mirrors
7993-
// the success path's append branch); otherwise reconvert from UI.
7994-
// Guard the conversion so a secondary failure can't crash the run.
79957961
if (!responseCommitted) {
79967962
try {
79977963
if (erroredNewUIMessages.length > 0) {
@@ -8014,17 +7980,12 @@ function chatAgent<
80147980
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
80157981
}
80167982
} catch {
8017-
// Keep the prior model accumulator if conversion fails.
80187983
erroredNewModelMessages = [];
80197984
erroredUIMessagesWithPartial = erroredUIMessages;
80207985
erroredNewUIMessages = erroredWireMessage ? [erroredWireMessage] : [];
80217986
}
80227987
}
80237988

8024-
// Fire onTurnComplete on the error path too — the docs promise it
8025-
// runs "after every turn, successful or errored" so customers can
8026-
// mark the turn failed. `responseMessage` carries any partial
8027-
// recovered above and `error` carries the thrown value.
80287989
if (onTurnComplete) {
80297990
try {
80307991
await tracer.startActiveSpan(

packages/trigger-sdk/test/chat-agent-source-stream-error.test.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,10 @@ describe("chat.agent managed loop — source-stream failure", () => {
8989

9090
const evt = turnCompletes[0]!;
9191

92-
// The turn is reported as errored, carrying the thrown transport error.
9392
expect(evt.finishReason).toBe("error");
9493
expect(evt.error).toBeInstanceOf(Error);
9594
expect((evt.error as Error).message).toBe("UND_ERR_BODY_TIMEOUT");
9695

97-
// The partial assistant output that streamed before the failure must be
98-
// preserved so persistence / recovery can keep it, instead of being
99-
// dropped (responseMessage: undefined).
10096
expect(evt.responseMessage).toBeDefined();
10197
expect(extractText(evt.responseMessage)).toBe("partial answer");
10298

@@ -148,8 +144,6 @@ describe("chat.agent managed loop — source-stream failure", () => {
148144
if (turn === 1) {
149145
return erroringSource("UND_ERR_BODY_TIMEOUT") as never;
150146
}
151-
// Second turn: the failed turn's partial assistant output must be in
152-
// the accumulated history the model now sees.
153147
turn2Messages = messages;
154148
return streamText({
155149
model: new MockLanguageModelV3({ doStream: async () => ({ stream: okStream() }) }),
@@ -416,8 +410,6 @@ describe("chat.createSession turn.complete() — source-stream failure", () => {
416410
await turn.complete(erroringSource("UND_ERR_BODY_TIMEOUT") as never);
417411
} catch (err) {
418412
caughtError = err;
419-
// The partial must be accumulated so persistence from the session
420-
// state keeps it, rather than being lost on the rethrow.
421413
uiMessagesAfterError = [...turn.uiMessages];
422414
await turn.done();
423415
}

0 commit comments

Comments
 (0)