@trigger.dev/sdk 4.5.12 → 4.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/commonjs/v3/ai.d.ts +154 -20
  2. package/dist/commonjs/v3/ai.js +1241 -387
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/chat.d.ts +7 -2
  5. package/dist/commonjs/v3/chat.js +22 -7
  6. package/dist/commonjs/v3/chat.js.map +1 -1
  7. package/dist/commonjs/v3/chat.test.js +13 -4
  8. package/dist/commonjs/v3/chat.test.js.map +1 -1
  9. package/dist/commonjs/v3/envvars.js.map +1 -1
  10. package/dist/commonjs/v3/sessions.d.ts +4 -10
  11. package/dist/commonjs/v3/sessions.js +73 -47
  12. package/dist/commonjs/v3/sessions.js.map +1 -1
  13. package/dist/commonjs/v3/test/mock-chat-agent.js +1 -0
  14. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  15. package/dist/commonjs/v3/test/test-session-handle.js +22 -23
  16. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  17. package/dist/commonjs/version.js +1 -1
  18. package/dist/esm/v3/ai.d.ts +154 -20
  19. package/dist/esm/v3/ai.js +1239 -387
  20. package/dist/esm/v3/ai.js.map +1 -1
  21. package/dist/esm/v3/chat.d.ts +7 -2
  22. package/dist/esm/v3/chat.js +22 -7
  23. package/dist/esm/v3/chat.js.map +1 -1
  24. package/dist/esm/v3/chat.test.js +13 -4
  25. package/dist/esm/v3/chat.test.js.map +1 -1
  26. package/dist/esm/v3/envvars.js.map +1 -1
  27. package/dist/esm/v3/sessions.d.ts +4 -10
  28. package/dist/esm/v3/sessions.js +73 -47
  29. package/dist/esm/v3/sessions.js.map +1 -1
  30. package/dist/esm/v3/test/mock-chat-agent.js +2 -1
  31. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  32. package/dist/esm/v3/test/test-session-handle.js +23 -24
  33. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  34. package/dist/esm/version.js +1 -1
  35. package/docs/ai-chat/client-protocol.mdx +8 -3
  36. package/docs/ai-chat/custom-agents.mdx +181 -46
  37. package/docs/ai-chat/patterns/recovery-boot.mdx +9 -2
  38. package/docs/ai-chat/patterns/version-upgrades.mdx +26 -6
  39. package/docs/ai-chat/pending-messages.mdx +5 -3
  40. package/docs/ai-chat/reference.mdx +26 -10
  41. package/docs/ai-chat/types.mdx +5 -1
  42. package/docs/deployment/atomic-deployment.mdx +12 -0
  43. package/docs/deployment/overview.mdx +7 -1
  44. package/docs/deployment/version-skew-protection.mdx +430 -0
  45. package/docs/github-actions.mdx +33 -5
  46. package/docs/github-integration.mdx +12 -0
  47. package/docs/self-hosting/env/webapp.mdx +1 -0
  48. package/docs/vercel-integration.mdx +43 -9
  49. package/docs/versioning.mdx +2 -0
  50. package/package.json +2 -2
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  title: "Version upgrades"
3
3
  sidebarTitle: "Version upgrades"
4
- description: "Gracefully migrate suspended chat agents to a new deployment using chat.requestUpgrade() and the continuation mechanism."
4
+ description: "Gracefully migrate chat agents to a new deployment using chat.requestUpgrade(), chat.endAndContinue(), and the continuation mechanism."
5
5
  ---
6
6
 
7
7
  Chat agent runs are pinned to the worker version they started on. When you deploy a new version, suspended runs resume on the **old** code. If your deploy includes breaking changes (new tools, changed schemas, updated API contracts), this can cause issues.
8
8
 
9
- `chat.requestUpgrade()` lets the agent opt out of the current run so the transport triggers a new one on the latest version.
9
+ `chat.requestUpgrade()` is the managed upgrade signal for `chat.agent()` and the `chat.createSession()` iterator. Fully hand-rolled custom agents use `chat.endAndContinue()` between turns to immediately hand the Session to a new run.
10
10
 
11
11
  ## How it works
12
12
 
@@ -151,14 +151,34 @@ export const myChat = chat
151
151
 
152
152
  This upgrades on **every** deploy, not just breaking changes. Good for fast-moving projects where you always want the latest code.
153
153
 
154
- ## Other agent types
154
+ ## Custom agents
155
155
 
156
- - **`chat.agent()`** and **`chat.createSession()`** use `chat.requestUpgrade()` as shown above
157
- - **`chat.customAgent()`** — you control the turn loop, so just `return` from `run()` when you want to exit
156
+ Use `chat.requestUpgrade()` with `chat.agent()`. With `chat.createSession()`, call `chat.requestUpgrade()`, then advance the iterator once more so it can exit normally. For an immediate handoff, close the iterator before calling `chat.endAndContinue()`. In a fully hand-rolled `chat.customAgent()` task, detach input listeners, persist the completed turn, write its boundary, then call `chat.endAndContinue()` and return immediately:
157
+
158
+ Close a `chat.createSession()` iterator between reads. If `return()` races a `next()` that is already waiting for input, it waits for that read to settle before the handoff can continue. Input dispatched while the iterator is closing is not yielded as a turn and remains available to the continuation unless you write another turn-complete boundary.
159
+
160
+ ```ts
161
+ // Detach any chat.messages.on() subscriptions you created.
162
+ stop.cleanup();
163
+ await persistMessages(conversation.uiMessages);
164
+ await chat.writeTurnComplete();
165
+ await chat.endAndContinue();
166
+ return;
167
+ ```
168
+
169
+ The continuation uses the same durable Session and receives `.in` records that the old run has not consumed. It starts on the latest deployed task version unless the Session's trigger configuration sets `lockToVersion`.
170
+
171
+ If input has been dispatched to the old run but should be processed by the continuation, detach the old listeners and skip the final `chat.writeTurnComplete()`. A turn-complete boundary acknowledges the latest input dispatched to the old run, so writing one after that dispatch would cause the continuation to resume past the input.
172
+
173
+ <Warning>
174
+ `chat.endAndContinue()` starts the successor but does not stop the calling run. Perform no more Session reads or writes after calling it, and return from the task. If the handoff fails, the promise rejects.
175
+ </Warning>
158
176
 
159
177
  ## Interaction with recovery boot
160
178
 
161
- `chat.requestUpgrade()` is a graceful exit the old run returns cleanly, never writing a partial assistant. The new continuation run boots with an empty `session.out` tail and the upgrade-trigger message on `session.in`. The trigger message dispatches as turn 1 on the new version via the normal continuation-wait path. [`onRecoveryBoot`](/ai-chat/patterns/recovery-boot) does NOT fire on this path — the hook is reserved for mid-stream interruptions (cancel / crash / OOM) where a partial assistant exists on the tail.
179
+ When `chat.requestUpgrade()` is handled before a turn starts, the SDK immediately hands the Session to a new run, which processes the same input on the latest version. When it is requested during a turn, including through `chat.createSession()`, the current turn finishes and the old run exits; the next input starts the continuation run.
180
+
181
+ Both are graceful exits. [`onRecoveryBoot`](/ai-chat/patterns/recovery-boot) does not fire — the hook is reserved for mid-stream interruptions (cancel, crash, or OOM) where a partial assistant exists on the tail.
162
182
 
163
183
  ## See also
164
184
 
@@ -10,7 +10,9 @@ When an AI agent is executing tool calls, users may want to send a message that
10
10
 
11
11
  By default (without `pendingMessages`), a message sent while the agent is responding never interrupts the in-flight response: it's buffered and processed as its own turn once the current turn completes, with multiple messages running sequentially in arrival order.
12
12
 
13
- The `pendingMessages` option enables steering instead, injecting user messages between tool-call steps via the AI SDK's `prepareStep`. Messages that arrive during streaming are queued and injected at the next step boundary. If there are no more step boundaries (single-step response or final text generation), the message becomes the next turn automatically.
13
+ The `pendingMessages` option enables steering instead, injecting user messages between tool-call steps via the AI SDK's `prepareStep`. Messages that arrive during streaming are queued and injected at the next step boundary. A message that is not injected becomes the next turn instead, whether that is because `shouldInject` returned `false` or because there were no more step boundaries (single-step response or final text generation). The backend handles that, so no client-side re-send is involved.
14
+
15
+ Injection is what needs wiring: the `pendingMessages` options only reach `streamText` if you spread `chat.toStreamTextOptions()` (or pass `prepareStep`). Without that, nothing injects, so every mid-turn message is answered as the next turn. Deferral does not depend on it.
14
16
 
15
17
  ## How it works
16
18
 
@@ -20,7 +22,7 @@ The `pendingMessages` option enables steering instead, injecting user messages b
20
22
  4. At the next `prepareStep` boundary (between tool-call steps), `shouldInject` is called
21
23
  5. If it returns `true`, the message is injected into the LLM's context
22
24
  6. A `data-pending-message-injected` stream chunk confirms injection to the frontend
23
- 7. If `prepareStep` never fires (no tool calls), the message becomes the next turn
25
+ 7. If `shouldInject` returns `false`, or `prepareStep` never fires (no tool calls), the message stays queued on the backend and is answered as the next turn
24
26
 
25
27
  ## Backend: chat.agent
26
28
 
@@ -310,7 +312,7 @@ function Chat({ chatId }: { chatId: string }) {
310
312
 
311
313
  ### Message lifecycle
312
314
 
313
- - **Steering messages** are sent via `transport.sendPendingMessage()` immediately. They appear as purple pending bubbles. If injected, they disappear from the overlay and render inline at the injection point. If not injected (no more step boundaries), they auto-send as the next turn when the response finishes.
315
+ - **Steering messages** are sent via `transport.sendPendingMessage()` immediately. They appear as purple pending bubbles. If injected, they disappear from the overlay and render inline at the injection point. If not injected, the backend answers them as the next turn once the response finishes; the client does not need to re-send them.
314
316
 
315
317
  - **Queued messages** stay client-side until the turn completes, then auto-send as the next turn via `sendMessage()`. They can be promoted to steering mid-stream by clicking "Steer instead".
316
318
 
@@ -396,7 +396,7 @@ Options for the `pendingMessages` field. See [Pending Messages](/ai-chat/pending
396
396
 
397
397
  | Option | Type | Required | Description |
398
398
  | -------------- | --------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------- |
399
- | `shouldInject` | `(event: PendingMessagesBatchEvent) => boolean \| Promise<boolean>` | No | Decide whether to inject the batch between tool-call steps. If absent, no injection. |
399
+ | `shouldInject` | `(event: PendingMessagesBatchEvent) => boolean \| Promise<boolean>` | No | Decide whether to inject the batch between tool-call steps. If absent, nothing is injected and the messages are answered as the next turn. Only consulted when `chat.toStreamTextOptions()` (or `prepareStep`) reaches `streamText`; without that nothing is injected and every mid-turn message becomes the next turn. |
400
400
  | `prepare` | `(event: PendingMessagesBatchEvent) => ModelMessage[] \| Promise<ModelMessage[]>` | No | Transform the batch before injection. Default: convert each via `convertToModelMessages`. |
401
401
  | `onReceived` | `(event: PendingMessageReceivedEvent) => void \| Promise<void>` | No | Called when a message arrives during streaming (per-message). |
402
402
  | `onInjected` | `(event: PendingMessagesInjectedEvent) => void \| Promise<void>` | No | Called after a batch is injected via prepareStep. |
@@ -504,13 +504,14 @@ All methods available on the `chat` object from `@trigger.dev/sdk/ai`.
504
504
  | `chat.createSession(payload, options)` | Create an async iterator for chat turns |
505
505
  | `chat.pipe(source, options?)` | Pipe a stream to the frontend (from anywhere inside a task) |
506
506
  | `chat.pipeAndCapture(source, options?)` | Pipe and capture the response; returns `{ message, status, error }` |
507
- | `chat.writeTurnComplete(options?)` | Signal turn complete; returns `{ lastEventId, sessionInEventId }` resume cursors |
507
+ | `chat.writeTurnComplete(options?)` | Signal turn complete; returns `{ lastEventId, sessionInEventId }` resume cursors. `sessionInEventId` is a lower bound, not the sequence of the record the turn answered |
508
508
  | `chat.createStopSignal()` | Create a managed stop signal wired to the stop input stream |
509
- | `chat.messages` | Input stream for incoming messages use `.waitWithIdleTimeout()` |
509
+ | `chat.messages` | Incoming message mailbox; supports non-consuming `.peek()` / `.hasPending()`, single-record `.next()`, `.on()`, and suspend-aware `.waitWithIdleTimeout()` |
510
510
  | `chat.local<T>({ id })` | Create a per-run typed local (see [`chat.local`](/ai-chat/chat-local)) |
511
511
  | `chat.createStartSessionAction(taskId, options?)` | Returns a server action that creates a chat Session + triggers the first run + returns a session-scoped PAT. Idempotent on `(env, externalId)`. |
512
512
  | `chat.waitForHandover(options)` | Wait for a [`chat.headStart`](/ai-chat/fast-starts#handover-with-custom-agents) handover signal in a custom loop. Returns the signal or `null`. `chat.MessageAccumulator` wraps this as `consumeHandover()` / `applyHandover()` |
513
513
  | `chat.requestUpgrade()` | End the current run after this turn so the next message starts on the latest agent version. Server-orchestrated handoff. |
514
+ | `chat.endAndContinue()` | In a hand-rolled custom agent, hand off the Session to a fresh continuation run. Call between turns after detaching input listeners, then return immediately. The promise rejects if the handoff fails. |
514
515
  | `chat.setTurnTimeout(duration)` | Override turn timeout at runtime (e.g. `"2h"`) |
515
516
  | `chat.setTurnTimeoutInSeconds(seconds)` | Override turn timeout at runtime (in seconds) |
516
517
  | `chat.setIdleTimeoutInSeconds(seconds)` | Override idle timeout at runtime |
@@ -546,15 +547,30 @@ Use this when you need [`InferChatUIMessage`](#inferchatuimessage) / typed `data
546
547
 
547
548
  ## `chat.withClientData`
548
549
 
549
- Returns a [`ChatBuilder`](/ai-chat/types#chatbuilder) with a fixed client data schema. All hooks and `run` get typed `clientData` without passing `clientDataSchema` in `.agent()` options.
550
+ Returns a [`ChatBuilder`](/ai-chat/types#chatbuilder) with a fixed client data schema. Managed-agent hooks and `run` get typed `clientData` without passing `clientDataSchema` in `.agent()` options. Custom agents parse `payload.metadata` on the initial payload and later input frames before passing it to user code.
550
551
 
551
552
  ```ts
552
- chat.withClientData<TSchema>({ schema: TSchema }): ChatBuilder<UIMessage, TSchema>;
553
+ chat.withClientData<TSchema extends TaskSchema>(config: {
554
+ schema: TSchema;
555
+ reportErrorAt?: "turn-end" | "arrival";
556
+ onValidationError?: (event: {
557
+ error: unknown;
558
+ payload: ChatTaskWirePayload;
559
+ }) => Promise<void> | void;
560
+ }): ChatBuilder<UIMessage, TSchema>;
553
561
  ```
554
562
 
555
- | Parameter | Type | Description |
556
- | --------- | ------------ | -------------------------------------------------- |
557
- | `schema` | `TaskSchema` | Zod, ArkType, Valibot, or any supported schema lib |
563
+ | Parameter | Type | Default | Description |
564
+ | ------------------- | --------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------ |
565
+ | `schema` | `TaskSchema` | required | Zod, ArkType, Valibot, or any supported schema lib |
566
+ | `reportErrorAt` | `"turn-end" \| "arrival"` | `"turn-end"` | When the client-visible `Invalid client data` error is written for a steering frame that failed validation mid-turn |
567
+ | `onValidationError` | `(event) => void` | — | Called when an input fails validation. Composes with the task-level `onClientDataValidationError` rather than replacing it |
568
+
569
+ `reportErrorAt` governs only the stream-visible error, and only for frames arriving on the steering subscription created by `chat.createSession({ pendingMessages })`. `"turn-end"` holds it until the turn closes, so a bad send cannot truncate an answer the user is already reading; `"arrival"` writes it as soon as validation fails, ending the response in progress. `onValidationError` and the task log fire on arrival in both modes, and the frame is never delivered as a turn either way. A raw `chat.messages.on()` subscription has no turn boundary to defer to, so it always reports through the callback and the task log without writing to the stream.
570
+
571
+ For `chat.customAgent()`, invalid client data is skipped. Async reads emit an error chunk followed by `turn-complete`. A `chat.messages.on()` subscription uses the task's `onClientDataValidationError` callback and task log instead, so an active response is not ended early. Without a schema, metadata is passed through unchanged.
572
+
573
+ Passing options directly to `chat.customAgent()` instead of through the builder uses the flat equivalents: `clientDataSchema`, `clientDataReportErrorAt`, and `onClientDataValidationError`.
558
574
 
559
575
  Full guide: [Typed client data](/ai-chat/types#typed-client-data-with-chatwithclientdata).
560
576
 
@@ -645,7 +661,7 @@ The `onEvent` callback receives a `ChatTransportEvent` (exported from `@trigger.
645
661
  | `message-send-failed` | `messageId?`, `source`, `error`, `status?`, `durationMs`, `partId?`, `bodyBytes?` | A send definitively failed after internal retries. Fires in addition to `useChat`'s `onError`. |
646
662
  | `stream-connected` | `resumed`, `lastEventId?`, `messageId?` | The SSE subscription to the session's output stream started delivering. `resumed: true` when reconnecting from a stored cursor (page reload) rather than following a fresh send. `lastEventId` is the cursor it connected from. |
647
663
  | `first-chunk` | `chunkType?`, `lastEventId?`, `messageId?`, `sinceSendMs?` | The first response chunk of a turn arrived. `sinceSendMs` is the delta from the last turn-producing send — time to first token without any bookkeeping. |
648
- | `turn-completed` | `lastEventId?`, `sessionInEventId?`, `messageId?`, `sinceSendMs?` | The agent's turn-complete control record arrived — the "finished answering" signal. `sinceSendMs` is the full turn latency; `sessionInEventId` is the agent's committed input-stream cursor. |
664
+ | `turn-completed` | `lastEventId?`, `sessionInEventId?`, `messageId?`, `sinceSendMs?` | The agent's turn-complete control record arrived — the "finished answering" signal. `sinceSendMs` is the full turn latency; `sessionInEventId` is the cursor the agent can safely resume its input stream from. Treat it as a lower bound: it is held back behind any message still waiting to be handled, so it can be below the sequence of the record this turn answered. Do not use it to decide whether a turn boundary belongs to your own send. |
649
665
  | `stream-error` | `error`, `status?` | The output stream failed unrecoverably. |
650
666
 
651
667
  `source` identifies the send path: `"submit-message"`, `"regenerate-message"`, `"steer"` (`sendPendingMessage`), `"action"` (`sendAction`), `"stop"` (`stopGeneration`), or `"head-start"`.
@@ -784,7 +800,7 @@ Send a custom action to the agent. Actions wake the agent from suspension and fi
784
800
  transport.sendAction(chatId: string, action: unknown): Promise<ReadableStream<UIMessageChunk>>
785
801
  ```
786
802
 
787
- The action payload is validated against the agent's `actionSchema` on the backend.
803
+ For managed `chat.agent()` tasks, the action payload is validated against the agent's `actionSchema` on the backend. Raw `chat.customAgent()` tasks receive it as `unknown` and must validate it themselves.
788
804
 
789
805
  ```tsx
790
806
  // Undo button
@@ -140,7 +140,7 @@ You can also import `InferChatUIMessage` from `@trigger.dev/sdk/ai` in non-React
140
140
 
141
141
  ## Typed client data with `chat.withClientData`
142
142
 
143
- `chat.withClientData({ schema })` returns a [ChatBuilder](#chatbuilder) that fixes the client data schema. All hooks and `run` receive typed `clientData` without needing `clientDataSchema` in `.agent()` options.
143
+ `chat.withClientData({ schema })` returns a [ChatBuilder](#chatbuilder) that fixes the client data schema. Managed-agent hooks and `run` receive typed `clientData` without needing `clientDataSchema` in `.agent()` options. A `.customAgent()` run receives the parsed schema output in `payload.metadata`, and `chat.createSession()` yields it as `turn.clientData`.
144
144
 
145
145
  ```ts
146
146
  import { chat } from "@trigger.dev/sdk/ai";
@@ -167,6 +167,10 @@ export const myChat = chat
167
167
  });
168
168
  ```
169
169
 
170
+ The schema runs at runtime for both `.agent()` and `.customAgent()`. Custom agents validate the initial payload and later `chat.messages` frames. Invalid frames are not passed to user code. Async reads emit an error chunk followed by `turn-complete`; `chat.messages.on()` reports through `onClientDataValidationError` and the task log so it does not end an active response. Without a schema, metadata is passed through unchanged.
171
+
172
+ `withClientData` also takes `reportErrorAt` and `onValidationError` alongside `schema`. See [chat.withClientData](/ai-chat/reference#chatwithclientdata) for both, and [Validating client data](/ai-chat/custom-agents#validating-client-data) for the custom-agent walkthrough.
173
+
170
174
  ## ChatBuilder
171
175
 
172
176
  Both `chat.withUIMessage()` and `chat.withClientData()` return a **ChatBuilder** — a chainable object that accumulates configuration before creating the agent with `.agent()`.
@@ -4,6 +4,18 @@ sidebarTitle: "Atomic deploys"
4
4
  description: "Use atomic deploys to coordinate changes to your tasks and your application."
5
5
  ---
6
6
 
7
+ <Warning>
8
+ **There's now a simpler way to do this.** [Version skew
9
+ protection](/deployment/version-skew-protection) solves the same problem without a second
10
+ deployment, without gating your app's deploy, and without setting `TRIGGER_VERSION` — and it covers
11
+ staging and preview as well as production. If you use the [Vercel
12
+ integration](/vercel-integration), its **automatic atomic deployments** setting is now deprecated
13
+ in favour of skew protection.
14
+
15
+ The manual workflows on this page still work, and remain the right answer if you specifically want
16
+ your application's deployment held back until your tasks have finished building.
17
+ </Warning>
18
+
7
19
  Atomic deploys in Trigger.dev allow you to synchronize the deployment of your application with a specific version of your tasks. This ensures that your application always uses the correct version of its associated tasks, preventing inconsistencies or errors due to version mismatches.
8
20
 
9
21
  ## How it works
@@ -122,6 +122,12 @@ If you want to set a global version to run all tasks against, you can use the `T
122
122
  TRIGGER_VERSION=20250228.1
123
123
  ```
124
124
 
125
+ <Tip>
126
+ If what you actually want is for each release of your app to run against the tasks built from the
127
+ same commit, you don't need to plumb version numbers around by hand. See [version skew
128
+ protection](/deployment/version-skew-protection).
129
+ </Tip>
130
+
125
131
  ### Child tasks and auto-version locking
126
132
 
127
133
  Trigger and wait functions version lock child task runs to the parent task run version. This ensures the results from child runs match what the parent task is expecting. If you don't wait then version locking doesn't apply.
@@ -155,7 +161,7 @@ Or from the dashboard:
155
161
 
156
162
  ![Trigger.dev dashboard showing the promote button](/deployment/promote-button.png)
157
163
 
158
- To learn more about skipping promotion and how this enables atomic deployments, see our [Atomic deployment](/deployment/atomic-deployment) guide.
164
+ To learn more about skipping promotion and how this enables atomic deployments, see our [Atomic deployment](/deployment/atomic-deployment) guide. To keep your app and tasks in sync without coordinating promotion at all, see [version skew protection](/deployment/version-skew-protection).
159
165
 
160
166
  ## Staging deploys
161
167