Agent Playground Streaming#
The Agent Playground delivers LLM responses via two response channels managed by AgentChatTransport β a custom transport class wired into the Vercel AI SDK useChat hook. The default channel is stream mode (Server-Sent Events, token-by-token); batch mode (single JSON response) is available as a fallback or manual override. The playground also keeps live streams alive across revision switches, so in-progress turns survive config updates.
Key source files (in the private web/oss + web/packages tree):
| File | Role |
|---|---|
AgentChatSlice/assets/AgentChatTransport.ts | Custom transport β stream and batch parsing |
AgentChatSlice/AgentChatPanel.tsx | useChat host β entityIdRef for live-revision sends |
packages/agenta-playground/src/state/execution/channelMode.ts | agentChannelModeAtom β Jotai atom for stream/batch toggle |
packages/agenta-playground/src/state/execution/agentNegotiation.ts | createNegotiatingFetch β automatic 406βbatch fallback |
packages/agenta-playground/src/state/execution/agentRequest.ts | buildAgentRequest β sets Accept header from channel mode |
Playground/Components/MainLayout/index.tsx | Stable "agent-generation-host" key to survive revision switches |
packages/agenta-playground-ui/src/components/ExecutionItems/index.tsx | agentSurfaceRef latch β prevents teardown during flag reload |
AgentChatTransport and Response Channels#
AgentChatTransport subclasses DefaultChatTransport from the Vercel AI SDK and overrides processResponseStream to handle both SSE and batch paths .
Channel selection is controlled by agentChannelModeAtom (a Jotai atom, default "stream"). The buildAgentRequest function reads this atom and sets the Accept header accordingly :
"stream"βAccept: text/event-streamβ response is an SSE stream conforming to Vercel AI SDK'suiMessageChunkSchema;processResponseStreamdelegates to the default SSE parser."batch"βAccept: application/jsonβ response is aWorkflowBatchResponse;AgentChatTransportcallsbatchJsonToUiMessageStreamto replay it as a synthetic one-shot chunk sequence:start β start-step β text/reasoning/tool parts β finish-step β finish.
The batch replay path handles flexible data.outputs shapes: {role, content} where content may be a string or an array of content blocks (tool use, reasoning). Tool blocks are normalized to tool-input-available / tool-output-available chunk types .
A kebab-menu toggle in PlaygroundVariantHeaderMenu lets users manually switch channels .
Automatic StreamβBatch Negotiation#
PR #4875 introduced createNegotiatingFetch β a fetch middleware that makes the channel switch transparent when a backend handler can't stream .
Fallback flow:
- Request sent with
Accept: text/event-stream. - If the backend responds HTTP 406 Not Acceptable,
createNegotiatingFetchre-issues the request withAccept: application/json. AgentChatTransportdetects the resolved mode and routes to the batch replay parser.- The user sees a single-frame response with no error β the fallback is fully transparent.
Non-406 errors are passed through untouched so useChat's onError handler surfaces them (e.g., "insufficient credit") rather than swallowing them .
Concurrency safety: the resolved mode is tracked in a WeakMap keyed to the response body stream, so overlapping requests each carry their own mode and cannot interfere . AgentChatTransport owns the NegotiatingFetch instance directly .
Stream Persistence Across Revision Switches#
Before PR #4997, switching the displayed revision (via self-commit or the config header) unmounted the agent chat component, aborting any live stream and losing in-progress turns .
Three coordinated fixes address this:
1. Stable key in MainLayout
The agent generation panel is assigned the static key "agent-generation-host" instead of using variantId as the key. A revision switch becomes a prop update (entityId change) rather than a remount, keeping useChat state and the active SSE connection intact .
2. entityIdRef in AgentChatPanel
A useRef captures the current entityId and is read at send time inside prepareSendMessagesRequest. This means subsequent turns in the same session are dispatched against the live revision's config, not the revision that was active when the session mounted β fixing the stale closure capture from the previous implementation .
3. Latch refs for async flag gaps
Workflow flags (the isAgent boolean) reload a beat after a revision swap, causing a brief window where the component might unmount:
agentHostRefinPlaygroundMainViewβ latchestruewhile the new revision loads; releases only once the entity definitively loads as non-agent .agentSurfaceRefinExecutionItemsβ prevents teardown of the agent surface during this gap .
localStorage note: The
useChathook skips mid-stream persistence to localStorage, so only completed turns are persisted. In-progress turns survive revision switches in memory only .