Work on agent streaming: $ARGUMENTS
Before Starting
- Search OpenAI docs for streaming patterns:
- Use
mcp__openai-docs__search_openai_docswith query "Agents SDK streaming" for RunStreamEvent types - Use
mcp__openai-docs__search_openai_docswith query "Agents SDK RunStreamEvent" for event shape reference
- Use
- Read the streaming infrastructure across repos:
/Users/joshuashepherd/Desktop/Dev/repos/ai-lab-agent/src/agents/shared/chatkit-bridge.ts—createStreamResponse()(SSE) +createPlainTextStreamResponse()(text)/Users/joshuashepherd/Desktop/Dev/repos/ai-lab-agent/src/agents/shared/stream-utils.ts—extractTextDelta()from RunStreamEventsrc/agents/shared/enhanced-agent-bridge.ts— bridge streaming pipelinesrc/agents/shared/chatkit-bridge.ts— local ChatKit bridge
- Read the client-side transport:
src/hooks/custom/use-standalone-chat.ts—TextStreamChatTransport, session ID capture, 90s timeout fallback
Dual Stream Formats
SSE (Server-Sent Events) — Primary
Content-Type: text/event-stream
function createStreamResponse(generator: AsyncGenerator<ChatKitEvent>): Response {
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
for await (const event of generator) {
const data = JSON.stringify(event);
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
}
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
Plain Text — Vercel AI SDK compatibility
Content-Type: text/plain (triggered by ?format=text or Accept: text/plain)
function createPlainTextStreamResponse(generator: AsyncGenerator<ChatKitEvent>): Response {
// Concatenates only text_delta content, strips event metadata
// Appends __AILAB_SESSION_ID__:uuid at the end for session capture
}
ChatKit Event Types
type ChatKitEvent =
| { type: 'text_delta'; content: string }
| { type: 'tool_call'; name: string; params: Record<string, unknown> }
| { type: 'tool_complete'; name: string; result: unknown; duration: number }
| { type: 'image'; url: string; alt?: string }
| { type: 'done'; sessionId: string; finalOutput?: string }
| { type: 'error'; message: string; code?: string }
| ProgressEvent;
type ProgressEvent = {
type: 'progress';
phase: 'initializing' | 'context' | 'agent' | 'thinking' | 'tool_call' | 'generating' | 'complete';
message?: string;
};
Extracting Text Deltas from SDK Events
The extractTextDelta() function handles multiple event shapes from RunStreamEvent:
function extractTextDelta(event: RunStreamEvent): string | null {
// Searches multiple paths:
// - event.delta
// - event.data.delta
// - event.data.output_text_delta
// - Recursively flattens nested objects/arrays
// Returns the first string content found, or null
}
Session Continuity
Server side
- Request carries
conversationId(orsessionIdalias) - Response
doneevent includessessionId(generated UUID or from request) - Conversation persisted to
ai_lab_lite_conversationstable
Client side
useStandaloneChatcaptures__AILAB_SESSION_ID__:uuidfrom plain-text stream- Stores sessionId in hook state
- Sends sessionId on next request for multi-turn continuity
// Client-side session extraction
const sessionMarker = '__AILAB_SESSION_ID__:';
const markerIndex = text.indexOf(sessionMarker);
if (markerIndex !== -1) {
const sessionId = text.slice(markerIndex + sessionMarker.length).trim();
setSessionId(sessionId);
// Strip marker from displayed text
text = text.slice(0, markerIndex);
}
Message Windowing
function windowMessages(messages: Message[], max: number = 50): Message[] {
return messages.slice(-max); // Keep only latest N to avoid token overflow
}
Debugging Streams
Common issues and how to investigate:
- Stream hangs/stalls: Check for unresolved promises in the generator. The client has a 90s timeout fallback.
- Events out of order: SSE guarantees ordering. If using plain-text, events are concatenated sequentially.
- Missing session ID: Check that
doneevent includes sessionId. In plain-text mode, verify the__AILAB_SESSION_ID__marker is appended. - Tool events not rendering: Verify the client-side SourcesPanel is parsing
tool_callandtool_completeevents. - CORS errors: Check the API route returns proper CORS headers for OPTIONS preflight.
Rules
- Always include
Cache-Control: no-cacheandConnection: keep-aliveon SSE responses - The
doneevent MUST includesessionIdfor conversation continuity - Plain-text format must append
__AILAB_SESSION_ID__:uuidmarker for client extraction - Window messages to prevent token overflow (default max 50)
- Handle stream errors gracefully — emit an
errorevent, then close the stream - Never buffer the entire response before streaming — send events as they arrive
- Test both SSE and plain-text formats when modifying stream behavior
- Check OpenAI docs MCP for any changes to RunStreamEvent types or streaming patterns