Uh oh!
There was an error while loading. Please reload this page.
feat(sdk): browser-side chat client + transport (3/5) - #3544
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| trigger: "preload", | ||
| ...(this.triggerConfigDefault?.basePayload ?? {}), |
There was a problem hiding this comment.
🔴 Spread order lets user's basePayload silently override required trigger: "preload"
In AgentChat.ensureStarted, the required trigger: "preload" value is set before spreading this.triggerConfigDefault?.basePayload. Because later spread keys override earlier ones in JavaScript, if a user's triggerConfig.basePayload happens to contain a trigger key, it silently overrides "preload". The comment at packages/trigger-sdk/src/v3/chat-client.ts:592-596 explicitly states this value is required: "Without this, AgentChat's first run skips both preload and start hooks, which is where customer apps typically upsert their Chat row." Compare how chatId is correctly placed after the spread (line 600) so it can't be overridden — trigger should follow the same pattern.
| trigger: "preload", | |
| ...(this.triggerConfigDefault?.basePayload??{}), | |
| ...(this.triggerConfigDefault?.basePayload??{}), | |
| trigger: "preload", |
Was this helpful? React with 👍 or 👎 to provide feedback.
| setPendingMsgs((prev) => { | ||
| const msg = prev.find((m) => m.id === id); | ||
| if (!msg || msg._mode !== "queued") return prev; | ||
| transport.sendPendingMessage(chatId, msg, metadata); | ||
| return prev.map((m) => (m.id === id ? { ...m, _mode: "steering" as const } : m)); | ||
| }); |
There was a problem hiding this comment.
🟡 Side effect inside setPendingMsgs state updater causes double message send in React strict mode
transport.sendPendingMessage(chatId, msg, metadata) is called inside the setPendingMsgs state updater function at line 410. React strict mode (development) double-invokes state updater functions to detect impurities — the first invocation's result is discarded, but the network side effect has already fired. Because the discarded first call sees the original prev state (with _mode: "queued"), and the second call also sees the same original prev, the message is sent to the backend twice.
Fix approach
Move the sendPendingMessage call outside the updater:
constmsg=pendingMsgs.find((m)=>m.id===id&&m._mode==="queued");if(!msg)return;transport.sendPendingMessage(chatId,msg,metadata);setPendingMsgs((prev)=>prev.map((m)=>(m.id===id ? { ...m,_mode: "steering"asconst} : m)));Prompt for agents
In usePendingMessages hook in packages/trigger-sdk/src/v3/chat-react.ts, the promoteToSteering callback calls transport.sendPendingMessage inside the setPendingMsgs state updater function (line 410). React strict mode double-invokes state updater functions for purity checks, so this network side effect fires twice in development. The fix is to extract the sendPendingMessage call out of the updater: read the current pendingMsgs from a ref or from the state snapshot before calling setPendingMsgs, send the message, then call setPendingMsgs with a pure updater that only changes _mode. Alternatively, use a separate useEffect triggered by a state change. The key constraint is that the updater function passed to setPendingMsgs must be side-effect-free.
Was this helpful? React with 👍 or 👎 to provide feedback.
dbc0034 to
64929b7Comparea60187c to
172b7c3Compare64929b7 to
9d8b67bCompare172b7c3 to
a63e60aCompare9d8b67b to
f83a0e2Comparea63e60a to
ce861fbComparef83a0e2 to
4cedf7cComparece861fb to
dc686d8Compare4cedf7c to
63cb53eComparedc686d8 to
6c3490cCompare63cb53e to
b850847Compare6c3490c to
9d26e60Compareb850847 to
e570acbCompare9d26e60 to
b971910Comparee570acb to
dd7d928Compareb971910 to
fa95095Comparedd7d928 to
46dd26aComparefa95095 to
dc95b98Compare46dd26a to
4703774Comparedc95b98 to
abb2cabCompare4703774 to
e7f6819Compareabb2cab to
3233503Comparee7f6819 to
c3486feCompare3233503 to
3b68877Comparec3486fe to
9d67168Compare3b68877 to
cac4fc5Compare9d67168 to
914a08fComparecac4fc5 to
a4bd26aCompareAdds chat.agent({...}), a session-aware task definition for AI chat
agents. The runtime sits on top of the Sessions primitive and handles:
- Delta-only wire: each /in/append carries at most one message; agent
rebuilds prior history at run boot from an S3 snapshot + session.out
replay tail. Awaited snapshot writes after every onTurnComplete keep
the chain durable across idle suspends.
- Lifecycle hooks: onChatStart, onTurnStart, onTurnComplete, onAction,
onValidateMessages, hydrateMessages (short-circuits snapshot+replay
if the customer owns history).
- chat.history read primitives for HITL flows (getPendingToolCalls,
getResolvedToolCalls, extractNewToolResults, findMessage, all/getChain).
- chat.local — per-run typed data with Proxy access + dirty tracking.
- chat.headStart — first-turn TTFC bridge via a customer HTTP handler.
- oomMachine — one-shot OOM-retry on a larger machine; cutoff derived
from latest trigger:turn-complete chunk on session.out.
- Actions are no longer turns — onAction may mutate via chat.history.*
and may return a StreamTextResult without bumping the turn counter.
- gen_ai.conversation.id stamped on chat spans + metrics for telemetry.
- Skills runtime (loadable per-agent and per-task) + agent skills bundling.
Snapshot + replay integration tests in apps/webapp/test/.
mockChatAgent test harness updated for the new wire.
Includes the chat-agent, chat-agent-delta-wire-snapshots,
chat-history-read-primitives, chat-head-start, chat-actions-no-turn,
chat-session-attributes, agent-skills, and mock-chat-agent-test-harness
changesets.The browser-facing half of chat.agent: a TriggerChatTransport that plugs into ai-sdk's useChat, plus AgentChat for direct programmatic use. Together they let a Next.js or React app drive a chat.agent task in trigger.dev cloud over SSE. - TriggerChatTransport (packages/trigger-sdk/src/v3/chat.ts): Vercel ai-sdk Transport implementation. Delta-only wire sends, SSE reconnection with lastEventId resume, stop/abort cleanup, dynamic accessToken refresh, X-Peek-Settled fast-close. - AgentChat (chat-client.ts): direct programmatic chat with the same underlying transport. - useTriggerChatTransport (chat-react.ts): React hook for the ai-sdk Transport. - chat-tab-coordinator: cross-tab leader election for shared SSE. - auth.ts: token plumbing for the transport. - packages/core/src/v3/chat-client.ts: shared envelope/wire types used by both browser and server runtime.
914a08f to
f694134Comparea4bd26a to
76e3cecComparef694134 to
13447b8Compareericallam
commented
May 12, 2026
Folded into #3543. The browser-side chat client + transport code (TriggerChatTransport, AgentChat, chat-tab-coordinator, chat-react) was tightly coupled to the SDK package.json subpath exports declared in L2 — splitting them across two PRs created install/typecheck mismatches at the boundary. Both halves now live in #3543. Stack is now 4 layers:
|
Layer 3 of 5 in the chat.agent stack split
The browser-facing half of chat.agent: a
TriggerChatTransportthatplugs into Vercel's ai-sdk
useChat, plusAgentChatfor directprogrammatic use. Lets a Next.js or React app drive a
chat.agenttask in trigger.dev cloud over SSE.
Targets
feature/chat-agent-runtime— merge after #3543Components
TriggerChatTransport(packages/trigger-sdk/src/v3/chat.ts):Vercel ai-sdk Transport implementation. Delta-only wire sends, SSE
reconnection with
lastEventIdresume, stop/abort cleanup, dynamicaccessTokenrefresh,X-Peek-Settledfast-close.AgentChat(chat-client.ts): direct programmatic chat with thesame underlying transport.
useTriggerChatTransport(chat-react.ts): React hook for theai-sdk Transport.
chat-tab-coordinator: cross-tab leader election for shared SSE.auth.ts: token plumbing for the transport.packages/core/src/v3/chat-client.ts: shared envelope/wire typesused by both browser and server runtime.
Stack