feat(sdk): browser-side chat client + transport (3/5)#3544
feat(sdk): browser-side chat client + transport (3/5)#3544ericallam wants to merge 1 commit intofeature/chat-agent-runtimefrom
Conversation
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.
|
|
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:
const msg = 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" as const } : 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.
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 L2Components
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