Documentation Index
Fetch the complete documentation index at: https://trigger.dev/docs/llms.txt
Use this file to discover all available pages before exploring further.
v4.5.0-rc.1 — two bug fixes
Patch release on top of4.5.0-rc.0. Upgrade with:Fixes
- Agent Skills silently missing in
trigger devfor projects whose task files readprocess.envat module top level (e.g. a third-party SDK client initialized at import). Skill folders now bundle into.trigger/skills/reliably regardless of which env vars are set when the CLI launches. (#3690) COULD_NOT_FIND_EXECUTORwhen a task’s definition is loaded viaawait import(...)from inside another task’srun()— common when lazy-loading sub-agent tasks. Runtime workers now register such tasks with a sentinel file context, and the catalog logs a one-time warning per task id. (#3688)
v4.5.0-rc.0 — AI Agents graduate from chat-prerelease
First release candidate of v4.5. Everything covered by the0.0.0-chat-prerelease-* entries below now ships under a stable semver tag. Install:4.5.0-rc.0 explicitly.)What’s in the box
chat.agent— multi-turn AI chat backends as durable Trigger.dev tasks. Lifecycle hooks, recovery from cancel/crash/OOM, version upgrades, all in. See Overview and Quick Start.- Sessions — the durable bi-directional stream primitive that backs
chat.agent. Use it directly for any pattern that needs durable bi-directional streaming across runs. See Sessions. useTriggerChatTransport— a custom AI SDKChatTransportforuseChat. No API routes. See Frontend.- Head Start — opt-in route handler that runs the first
streamTextstep in your warm server while the agent boots in parallel. Cuts cold-start TTFC roughly in half. See Fast starts. - AI Prompts — code-defined, deploy-versioned templates with dashboard overrides for text + model. Integrates with
chat.agentviachat.prompt.set()+chat.toStreamTextOptions(). See Prompts. ai.toolExecute— wire any Trigger subtask in as theexecuteof an AI SDKtool(). See Sub-agents.
Compatibility
@trigger.dev/sdk@4.5.0-rc.0 requires ai ^5.0.0 || ^6.0.0 (Vercel AI SDK), React ^18.0 || ^19.0 (for the chat/react subpath), and Node.js >=18.20.0. Full matrix on the API Reference.Docs
This release ships with a refreshed AI Agents documentation set covering Backend, Frontend, Sessions, Lifecycle hooks,chat.local, the Patterns library, Testing, and a full API Reference.Recovery boot — context-preserving continuation after cancel / crash / OOM
When achat.agent run dies mid-stream (the user cancels, the worker OOMs, an unhandled exception kills the process), the next continuation run now reconstructs the conversation context automatically. Follow-ups like “keep going” continue the partial response; fresh follow-ups like “scrap that, what’s 7+8?” abandon it and answer the new question. No customer code required.Under the hood: the boot now reads BOTH stream tails — session.out for any partial assistant the dead run was streaming, session.in for any user messages it never acknowledged — and splices [firstInFlightUser, partialAssistant] onto the chain when both are present. The model sees full prior context plus the latest user message.For policies different from “preserve context” — drop the partial entirely, synthesize tool results for an interrupted tool call, emit a recovery banner to the UI — register the new onRecoveryBoot hook:settledMessages, inFlightUsers, partialAssistant, pendingToolCalls, previousRunId, cause, and a lazy writer. Return any of chain, recoveredTurns, or beforeBoot to override the default. Agents using hydrateMessages skip the hook — customer-owned persistence is the source of truth.Also retracts the OOM resilience caveat: model context on retry is no longer “incomplete” without hydrateMessages. The smart default reconstructs full context from session.out replay.See Recovery boot for the full guide.session.out is now bounded — header-form control records + per-turn trim
Long-lived chats were accumulating session.out records forever (every turn appends; nothing trimmed). The Sessions dashboard re-streamed the entire history from seq_num=0 on every page load, and OOM-retry boot scanned the whole stream to find the last turn-complete.After this release session.out stays roughly one turn long forever at steady state. After each turn-complete, the agent appends an S2 trim command record pointing back to the previous turn-complete’s seq_num. Full conversation history continues to live in the durable S3 snapshot, not on the stream. Resume across a single turn boundary still works (the previous turn-complete is still on the stream and S2’s eventually-consistent trim window gives 10-60s of grace); resume across multiple turns of inactivity falls back to the snapshot.What changed on the wire
trigger:turn-complete and trigger:upgrade-required are no longer JSON data chunks on session.out. They’re now header-form control records under a uniform trigger-control namespace:chunk.type into a trigger-control header value. Body is always empty; metadata that previously rode in the chunk (e.g. publicAccessToken) now rides on sibling headers.turn-complete also picks up a new optional sibling header — ["session-in-event-id", "<seq>"] — carrying the agent’s committed-consume cursor on .in as of this turn. It’s an agent-internal contract that lets the next worker boot seed its .in SSE subscription past already-processed user messages, without relying on a wall-clock-derived dedup cutoff. Custom transports should ignore the header; it has no client-side meaning.Custom transport implementers
Built-in SDK transports (TriggerChatTransport, AgentChat) handle this transparently — onTurnComplete fires the same way with the same payload. Custom transports filtering on chunk.type === "trigger:turn-complete" need to switch to the header-based filter:trim) is documented at Records on session.out.Sessions dashboard snapshot read
The Sessions detail page in the trigger.dev dashboard now reads the agent’s S3 snapshot first via a presigned URL, then SSE-tails fromsnapshot.lastOutEventId. Bandwidth and time-to-first-render are O(unread turns) instead of O(session lifetime). Sessions that registered a hydrateMessages hook (which skips snapshot writes) show only the most recent turn — those customers typically have their own DB-backed dashboards.Breaking surface
- Custom transports parsing
chunk.typefor turn-complete / upgrade-required must switch to thetrigger-controlheader check. - Snapshot consumers should import
ChatSnapshotV1/ChatSnapshotV1Schemafrom@trigger.dev/core/v3(now an exported shape, not SDK-internal).
Docs
- Records on
session.out— full filter rule for data / control / command records. - Resuming a stream — explicit single-turn vs multi-turn-away semantics.
turn-completecontrol record andupgrade-requiredcontrol record — replaced the old chunk-shape docs.
512 KiB /in/append ceiling removed for long chats — slim wire + S3 snapshot
chat.agent long-running chats with heavy tool results were hitting the realtime API’s 512 KiB body cap on /realtime/v1/sessions/{id}/in/append once the accumulated UIMessage[] history (which the wire shipped in full on every send) crossed the limit. The 413 surfaced as a CORS error in browsers and stalled chats around turn 10–30 with tool use.The wire is now delta-only: each .in/append carries at most one new UIMessage (the new user turn or a tool-approval response) instead of the full history. The agent rebuilds prior history at run boot from a durable JSON snapshot in object storage plus a replay of the session.out tail. The 512 KiB ceiling stops being pressure — slim payloads are normally a few KB regardless of chat length.What changed
ChatTaskWirePayload:messages: UIMessage[]is removed. Replaced bymessage?: UIMessage(singular, optional) and a dedicatedheadStartMessages?: UIMessage[]field used only bychat.headStartfirst-turn handover.- Run boot: when
hydrateMessagesis not registered, the runtime readspackets/{projectRef}/{envSlug}/sessions/{sessionId}/snapshot.jsonfrom object storage and replays anysession.outchunks landed since the snapshot’s cursor. Snapshot writes happen after everyonTurnComplete, awaited so they survive an idle suspend. hydrateMessagesshort-circuit: registering the hook skips snapshot read/write and replay entirely. Customer is the source of truth for history, same as today.hydrateMessages.incomingMessages: now consistently 0-or-1-length across every trigger type. Previouslyregenerate-messageand continuations occasionally shipped full history; they now ship none.onChatStartis now once-per-chat: fires only on the chat’s very first user message; does NOT fire on continuation runs (post-endRun, post-waitpoint-timeout, post-chat.requestUpgrade) or on OOM-retry attempts. ThecontinuationandpreviousRunIdfields onChatStartEventare now@deprecated(alwaysfalse/undefinedwhen the hook fires). Drop anyif (continuation) return;gates fromonChatStart— they’re now unreachable. For per-turn setup that runs on continuations too, move toonTurnStart.- Continuation boot payload: the server now strips
message/messages/triggerfrom the cachedbasePayloadon continuation runs, and the SDK enters a new continuation-wait branch that waits silently onsession.infor the next user message. Fixes a phantom-turn bug where stale boot-payload fields were replayed on every resume. - OOM-retry boot: uses the snapshot’s
lastOutTimestampas thesession.incutoff, saving one stream subscription per retry. - Built-in transports:
TriggerChatTransport,AgentChat, mid-stream pending-message handling, andchat.headStartroute handler all updated to the slim shape. Existing customer code callingtransport.sendMessage(...)/agentChat.sendMessage(...)is unaffected — the change is below those surfaces.
Object store configuration
Snapshot read/write reuses Trigger.dev’s existing object-store infrastructure — the same presigned-URL routes used for large payloads. SetOBJECT_STORE_* env vars on your webapp deployment if you haven’t already; MinIO works locally via OBJECT_STORE_DEFAULT_PROTOCOL.If no object store is configured and no hydrateMessages hook is registered, conversations don’t survive run boundaries (the runtime logs a warning at registration time). Either configure an object store or register hydrateMessages.Breaking surface
- Custom transports: any code constructing
ChatTaskWirePayloaddirectly must dropmessagesand usemessage. See the rewritten Client Protocol. - Client-side
setMessagesno longer round-trips: full-history mutations on the client never reached the agent before this release either, but the slim wire makes that explicit. Use server-sidechat.history.set()insideonTurnStartfor compaction. - Custom server-to-server senders: code calling
apiClient.appendToSessionInput(sessionId, ...)or hitting/realtime/v1/sessions/{id}/in/appenddirectly must switch to the slim shape.
Docs
- Rewritten Client Protocol — slim payload, new
headStartMessagesfield, new “How history is rebuilt” and “Head-start protocol caveat” sections. - New Persistence and replay — end-to-end walkthrough of the snapshot model, OOM-retry interaction, crash semantics,
hydrateMessagesshort-circuit. - New Tool result auditing — the
extractNewToolResults+onTurnComplete/hydrateMessagespattern for HITL audit logging. - v4.5 section of the upgrade guide — migration steps for custom transports and
hydrateMessagesconsumers. hydrateMessages,onChatStart— clarifications on the newincomingMessagesandmessagesshapes.
chat.history read primitives for HITL flows
Customers building human-in-the-loop tools were re-implementing the same accumulator-walking logic to figure out which tool calls were pending, which were resolved, and which results in an incoming wire message were actually new. Lifted into the SDK as five new methods on chat.history:| Method | Description |
|---|---|
chat.history.getPendingToolCalls() | Tool calls on the most recent assistant message in input-available state — gates fresh user turns during HITL. |
chat.history.getResolvedToolCalls() | All tool calls in the chain in output-available or output-error state. |
chat.history.extractNewToolResults(message) | Tool results in message whose toolCallId is not already resolved on the chain. Most useful in hydrateMessages against an incoming wire message, before the runtime merges it. |
chat.history.getChain() | Same as chat.history.all() — alias that reads better alongside parent-aware APIs. |
chat.history.findMessage(messageId) | Direct lookup; undefined if absent. |
chat.history and Human-in-the-loop.Fix: HITL addToolOutput resume preserves the assistant message id
In some HITL flows the AI SDK regenerated the assistant message id when the user’s addToolOutput answer round-tripped back to the agent. The fresh id slipped past the runtime’s id-based merge, leaving the resolved tool answer attached to a sibling assistant message instead of the head, which broke downstream dedup and rendered the tool answer twice.The runtime now records toolCallId → head messageId whenever an assistant with tool parts lands in the accumulator and rewrites the incoming id back via that map before the merge. Customers who had a content-match workaround for this can drop it.chat.agent actions are no longer turns
Submitting an action via transport.sendAction() previously fell through to the regular turn machinery, calling onTurnStart, run(), onTurnComplete, etc. — meaning every action fired an LLM call by default. The workaround was a chat.local-based skipModelCall flag read in run().Actions now fire hydrateMessages and onAction only. No onTurnStart / prepareMessages / onBeforeTurnComplete / onTurnComplete, no run() invocation, no turn-counter increment. The trace span is named chat action instead of chat turn N.onAction’s return type widens: returning void is side-effect-only (default); returning a StreamTextResult, string, or UIMessage produces a model response that’s auto-piped back to the frontend.Migration
If you hadrun() branching on payload.trigger === "action" for a model response, return your streamText(...) from onAction instead. If you persisted in onTurnComplete, do that work inside onAction. For state-only actions, just remove the skip-the-model workaround.onAction handler is configured now console.warn once and are ignored — previously they silently fell through to run() with an empty wire payload.Fix: duplicate turn after chat.agent idle-suspends
Every message sent to a chat.agent after the run idle-suspended produced two turns on the agent side instead of one — same user message, two LLM calls. Internal session-stream reconnect logic was racing the waitpoint and feeding the just-consumed message back into the next turn’s input buffer. No public API change.chat.headStart — fast first-turn for chat.agent
A new opt-in flow that cuts first-turn TTFC roughly in half by running step 1’s LLM call in your warm process while the chat.agent run boots in parallel. On the LLM’s tool-calls boundary, ownership of the durable stream hands over to the agent for tool execution and step 2+. Pure-text first turns finish on the customer side with no LLM call from the trigger run at all.Measured on claude-sonnet-4-6 (same model both sides): TTFT 2801ms → 1218ms (−57%), total turn 4180ms → 2345ms (−44%). With Head Start, first-text time is essentially the LLM TTFB floor.Setup
app/api/chat/route.ts
components/chat.tsx
Bundle isolation
Tool schemas (description + inputSchema) live in their own module that imports only ai and zod. The agent task imports those schemas and adds heavy execute fns. The route handler imports schemas only — keeping the warm-process bundle light is what makes the win possible. Runtime “strip executes” helpers don’t solve this — bundlers resolve imports at build time. See Fast starts → Head Start setup for the full split.Compared to Preload
Preload eagerly triggers the run on page load (good when you’re confident the user will send a message — trades idle compute for fast TTFC). Head Start gates the run on a real first message — no idle compute, customer’s process runs step 1 directly. Pick one per chat.Works on every runtime
chat.headStart returns a standard Web Fetch handler — (req: Request) => Promise<Response> — so it slots into Next.js App Router, Hono, SvelteKit, Remix / React Router v7, TanStack Start, Astro, Nitro/Nuxt, Elysia, Cloudflare Workers, Bun, Deno, and any other runtime that speaks Web Fetch. Verified runtimes: Node 18+, Bun, Deno, Workers, Vercel (Node and Edge), Netlify (Functions and Edge).For Node-only frameworks (Express, Fastify, Koa, raw node:http), the SDK ships chat.toNodeListener(handler) — converts any Web Fetch handler into a Node (req, res) listener with proper streaming, header translation, and client-disconnect propagation.Docs
- New Head Start guide — bundle isolation, schema/execute split, route handler setup, transport option, lifecycle, limitations.
- Reference —
headStarttransport option.
Resilient SSE reconnection
The chat transport now retries indefinitely on network drops with bounded exponential backoff (100ms initial, 5s cap, 50% jitter) instead of giving up after 5 attempts. Reconnects are immediate ononline, on tab refocus after a long background, and on Safari bfcache restore (pageshow with event.persisted).A 60s stall detector catches silent-dead-socket cases on mobile where the OS killed the TCP socket without the reader noticing. A 30s per-attempt fetch timeout prevents stuck connections from blocking the retry loop.Resume continues to use Last-Event-ID, so no chunks are lost when the connection comes back. No public API change — these are defaults on TriggerChatTransport. Customers who built hasActiveStream / isStreaming flag tracking on their side can drop it: the transport handles the silent-but-stale case internally now.SSEStreamSubscription (used by TriggerChatTransport and AgentChat) gained retryNow() and forceReconnect() for callers writing custom transports, plus options to tune maxRetries / retryDelayMs / maxRetryDelayMs / retryJitter / fetchTimeoutMs / stallTimeoutMs / nonRetryableStatuses. 404 and 410 short-circuit retry by default (stream gone / session closed).chat.agent now runs on Sessions
Every chat is backed by a durable Session row that outlives any single run. externalId = your chat ID, type = "chat.agent". Under the hood:- Output chunks stream on
session.out(was a run-scopedstreams.writer("chat")). - Client messages and stops land on
session.inas aChatInputChunktagged union (was two run-scopedstreams.inputdefinitions). - Wire endpoints moved from
/realtime/v1/streams/{runId}/...to/realtime/v1/sessions/{sessionId}/.... See the rewritten Client Protocol.
chat.agent(), TriggerChatTransport, AgentChat, chat.stream / chat.messages / chat.stopSignal) is unchanged — existing apps keep working. What’s new is:- Cross-run resume is free. A chat you were in yesterday resumes against the same
sessionIdtoday, even if the original run long since exited. No more lost conversations when a run idle-times-out. - Inbox views via
sessions.list({type: "chat.agent"}). Enumerate every chat in your environment, filter by tag or status. TriggerChatTaskResult.sessionId+ChatTaskRunPayload.sessionId— you can reach into the raw session viasessions.open(payload.sessionId)for advanced cases (writing from a sub-agent, custom transport).- Dashboard Agent tab resolves via
sessionIdand stays in sync with the live stream across runs.
X-Session-Settled — fast reconnect on idle chats
When a client reconnects to session.out and the tail record is a trigger:turn-complete marker (agent finished a turn, idle-waiting or exited), the server sets X-Session-Settled: true and uses wait=0 on the underlying S2 read. The SSE drains any remaining records then closes in ~1s instead of long-polling for 60s.Practical impact: TriggerChatTransport.reconnectToStream no longer needs a client-side isStreaming flag. You can drop the field from your persisted ChatSession state entirely — the server decides. Existing callers that still persist isStreaming are unaffected; reconnectToStream keeps the fast-path short-circuit when it’s false.Migration
See the Sessions Upgrade Guide for the full step-by-step — auth callback split, persistedChatSession shape, server-side helpers (chat.createStartSessionAction, chat.createAccessToken for renewal), and the clientData validation pivot.Docs
- Rewritten Client Protocol — full wire format for the new
/realtime/v1/sessions/{sessionId}/...endpoints, JWT scopes, S2 direct-write credentials, andLast-Event-IDresume. - Database persistence pattern — new
chatId-keyedChatSessionshape (no morerunId) and a warning on theonTurnCompleterace that requires a single atomic write ofmessages+lastEventId. - Reference — added
chat.createStartSessionAction,chat.createAccessToken,ChatInputChunk,TriggerChatTaskResult.sessionId,ChatTaskRunPayload.sessionId. The old run-scoped stream-ID constants are gone. - Refreshed Backend, Frontend, Server Chat, Quick start, Overview, Types, Error handling, and Testing for the session-based wiring.
Agent Skills
Ship reusable capabilities as folders — aSKILL.md plus optional scripts, references, and assets. The agent sees short descriptions in its system prompt, loads full instructions on demand via loadSkill, and invokes bundled scripts via bash — no manual wiring.skills.define({ id, path }) registers the skill; the CLI bundles the folder into the deploy image. chat.skills.set([...]) activates skills for the run; chat.toStreamTextOptions() auto-injects the preamble and tools.See the new Agent Skills guide.chat.endRun() — exit on your own terms
New imperative API to exit the loop after the current turn completes, without the upgrade-required signal that chat.requestUpgrade() sends. Use for one-shot agents, budget-exhausted exits, or goal-reached completions.onBeforeTurnComplete / onTurnComplete fire, the turn-complete chunk is written, and the run exits instead of suspending. Callable from run(), chat.defer(), onBeforeTurnComplete, or onTurnComplete. See Ending a run on your terms.finishReason on turn-complete events
TurnCompleteEvent and BeforeTurnCompleteEvent now include the AI SDK’s finishReason ("stop" | "tool-calls" | "length" | "content-filter" | "error" | "other"). Clean signal for distinguishing a normal turn end from one paused on a pending tool call (HITL flows like ask_user):chat.pipe() flows or aborted streams. See the new Human-in-the-loop pattern.User-initiated compaction pattern
The Compaction guide now covers how to wire a “Summarize conversation” button or/compact slash command via actionSchema + onAction. The agent summarizes on demand, rewrites history with chat.history.set(), and short-circuits the LLM call for action turns.Needed a small type fix for this: ChatTaskPayload.trigger now correctly includes "action", so run() handlers can short-circuit with if (trigger === "action") return when an action doesn’t need a response.Human-in-the-loop pattern page
New Human-in-the-loop page walks throughask_user-style mid-turn user input end-to-end: defining a no-execute tool, rendering pending tool calls on the frontend with addToolOutput + sendAutomaticallyWhen, detecting paused turns via finishReason, and two persistence strategies (overwrite vs. checkpoint nodes).Offline test harness for chat.agent
@trigger.dev/sdk/ai/test now ships mockChatAgent, a harness that drives a chat.agent definition through real turns without network or task runtime. Send messages, actions, and stop signals; inspect emitted chunks; assert on hook order.Dependency injection via locals
setupLocals pre-seeds locals before run() starts — the pattern for injecting database clients, service stubs, and other server-side dependencies that shouldn’t leak through untrusted clientData:locals.get(dbKey). Falls through to the production client in real runs.See Testing.runInMockTaskContext — lower-level test harness
@trigger.dev/core/v3/test now exports runInMockTaskContext for unit-testing any task code offline (not just chat agents). Installs in-memory managers for locals, lifecycleHooks, runtime, inputStreams, and realtimeStreams, plus a mock TaskContext. Drivers let you push data into input streams and inspect chunks written to output streams.Multi-tab coordination
Prevent duplicate messages when the same chat is open in multiple browser tabs. Enable withmultiTab: true on the transport.BroadcastChannel. When the active tab’s turn completes, any tab can send next. Crashed tabs are detected via heartbeat timeout (10s).See Multi-tab coordination and useMultiTabChat.Error stack truncation
Large error stacks no longer OOM the worker process. Stacks are capped at 50 frames (top 5 + bottom 45), individual lines at 1024 chars, messages at 1000 chars. Applied inparseError, sanitizeError, and OTel span recording.Fix: resume: true hangs on completed turns
When refreshing a page after a turn completed, useChat with resume: true would hang indefinitely — reconnectToStream opened an SSE connection that never received data.Added isStreaming to session state. The transport sets it to true when streaming starts and false on trigger:turn-complete. reconnectToStream returns null immediately when isStreaming is false, so resume: initialMessages.length > 0 is now safe to pass unconditionally.The flag flows through onSessionChange and is restored from sessions — no extra persistence code needed.hydrateMessages — backend-controlled message history
Load message history from your database on every turn instead of trusting the frontend accumulator. The hook replaces the built-in linear accumulation entirely — the backend is the source of truth.chat.history — imperative message mutations
Modify the accumulated message history from any hook or run():Custom actions — actionSchema + onAction
Send typed actions (undo, rollback, edit) from the frontend via transport.sendAction(). Actions wake the agent, fire onAction, then trigger a normal run() turn.transport.sendAction(chatId, { type: "undo" })
Server: agentChat.sendAction({ type: "undo" })See Actions and Sending actions.chat.response — persistent data parts
Added chat.response.write() for writing data parts that both stream to the frontend AND persist in onTurnComplete’s responseMessage and uiMessages.data-* chunks written via lifecycle hook writer.write() now automatically persist to the response message, matching the AI SDK’s default semantics. Add transient: true for ephemeral chunks (progress indicators, status updates).See Custom data parts.Tool approvals
Added support for AI SDK tool approvals (needsApproval: true). When the model calls a tool that needs approval, the turn completes and the frontend shows approve/deny buttons. After approval, the updated assistant message is sent back and matched by ID in the accumulator.sendAutomaticallyWhen and addToolApprovalResponse from useChat. See Tool approvals.transport.stopGeneration(chatId)
Added stopGeneration method to TriggerChatTransport for reliable stop after page refresh / stream reconnect. Works regardless of whether the AI SDK passes abortSignal through reconnectToStream.generateMessageId support
generateMessageId can now be passed via uiMessageStreamOptions to control response message ID generation (e.g. UUID-v7). The backend automatically passes originalMessages to toUIMessageStream so message IDs are consistent between frontend and backend.Bug fixes
onTurnCompletenot called: FixedturnCompleteResult?.lastEventIdTypeError that silently skippedonTurnCompletewhenwriteTurnCompleteChunkreturned undefined in dev.- Stop during streaming: Added 2s timeout on
onFinishPromisesoonBeforeTurnCompleteandonTurnCompletefire even when the AI SDK’sonFinishdoesn’t fire after abort. toStreamTextOptionswithoutchat.prompt.set():prepareStepinjection (compaction, steering, background context) now works even when the user passessystemdirectly tostreamTextinstead of usingchat.prompt.set().- Background queue vs tool approvals: Background context injection is now skipped when the last accumulated message is a
toolmessage, preventing it from breakingstreamText’scollectToolApprovals.

