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:
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.
dbc0034 to
64929b7
Compare
a60187c to
172b7c3
Compare
64929b7 to
9d8b67b
Compare
172b7c3 to
a63e60a
Compare
9d8b67b to
f83a0e2
Compare
a63e60a to
ce861fb
Compare
f83a0e2 to
4cedf7c
Compare
ce861fb to
dc686d8
Compare
4cedf7c to
63cb53e
Compare
dc686d8 to
6c3490c
Compare
63cb53e to
b850847
Compare
6c3490c to
9d26e60
Compare
b850847 to
e570acb
Compare
9d26e60 to
b971910
Compare
e570acb to
dd7d928
Compare
b971910 to
fa95095
Compare
dd7d928 to
46dd26a
Compare
fa95095 to
dc95b98
Compare
46dd26a to
4703774
Compare
dc95b98 to
abb2cab
Compare
4703774 to
e7f6819
Compare
abb2cab to
3233503
Compare
e7f6819 to
c3486fe
Compare
3233503 to
3b68877
Compare
c3486fe to
9d67168
Compare
3b68877 to
cac4fc5
Compare
9d67168 to
914a08f
Compare
cac4fc5 to
a4bd26a
Compare
Adds 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
f694134
Compare
a4bd26a to
76e3cec
Compare
f694134 to
13447b8
Compare
|
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