SSE Streaming
When enable_streaming is set to true on an agent endpoint, the execution endpoint returns Server-Sent Events (SSE) instead of a blocking JSON response. This lets you show real-time progress to users as the agent thinks, calls tools, and produces output.
Streaming is enabled per endpoint. Set enable_streaming: true when creating or updating the endpoint. All calls to that endpoint will return SSE responses.
Event Types
The SSE stream emits the following event types in order:
| Event | When | Payload |
|---|---|---|
thinking | Agent is processing | { status, message, iteration? } |
tool_call | Agent is invoking a tool | { iteration, tool, arguments } |
tool_result | Tool execution completed | { iteration, tool, success, latency_ms } |
done | Final output ready | { output, outputParsed?, tool_calls, usage } |
error | Execution failed | { message } |
Event Flow Example
A typical streaming session looks like this:
event: thinking
data: {"type":"thinking","status":"starting","message":"Resolving workspace..."}
event: thinking
data: {"type":"thinking","status":"loading_tools","message":"Loading workspace tools..."}
event: thinking
data: {"type":"thinking","status":"tools_loaded","message":"Loaded 12 tools"}
event: thinking
data: {"type":"thinking","status":"llm_call","message":"Calling gpt-4o-mini...","iteration":1}
event: tool_call
data: {"type":"tool_call","iteration":1,"tool":"github_list_issues","arguments":{"repo":"acme/api","state":"open"}}
event: tool_result
data: {"type":"tool_result","iteration":1,"tool":"github_list_issues","success":true,"latency_ms":234}
event: thinking
data: {"type":"thinking","status":"llm_call","message":"Calling gpt-4o-mini...","iteration":2}
event: done
data: {"type":"done","output":"Found 3 open issues...","tool_calls":[{"name":"github_list_issues","arguments":{"repo":"acme/api","state":"open"},"result":"...","latencyMs":234,"success":true}],"usage":{"tokens_input":1245,"tokens_output":189,"cost_cents":0.34,"latency_ms":2891,"iterations":2}}JavaScript: fetch + ReadableStream
The recommended approach for web applications. This gives you full control over parsing and error handling:
async function callAgentStreaming(slug: string, input: string, apiKey: string) {
const response = await fetch(`https://api.aerostack.dev/api/run/${slug}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ input }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
switch (data.type) {
case 'thinking':
console.log(`[${data.status}] ${data.message}`);
break;
case 'tool_call':
console.log(`Calling tool: ${data.tool}`);
break;
case 'tool_result':
console.log(`Tool ${data.tool}: ${data.success ? 'OK' : 'FAILED'} (${data.latency_ms}ms)`);
break;
case 'done':
console.log('Final output:', data.output);
console.log('Usage:', data.usage);
return data;
case 'error':
throw new Error(data.message);
}
}
}
}
}JavaScript: EventSource
For simpler use cases where you do not need to send custom headers. Note that EventSource only supports GET requests, so you will need a wrapper library like eventsource-parser or use the fetch approach above for POST requests.
// Using eventsource-parser (works with POST)
import { createParser } from 'eventsource-parser';
async function streamAgent(slug: string, input: string, apiKey: string) {
const response = await fetch(`https://api.aerostack.dev/api/run/${slug}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ input }),
});
const parser = createParser((event) => {
if (event.type === 'event' && event.data) {
const data = JSON.parse(event.data);
handleEvent(data);
}
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
parser.feed(decoder.decode(value, { stream: true }));
}
}
function handleEvent(event: any) {
switch (event.type) {
case 'thinking':
updateStatusUI(event.message);
break;
case 'tool_call':
addToolCallToUI(event.tool, event.arguments);
break;
case 'tool_result':
updateToolResultUI(event.tool, event.success);
break;
case 'done':
showFinalOutput(event.output, event.usage);
break;
case 'error':
showError(event.message);
break;
}
}React Example
import { useState, useCallback } from 'react';
function useAgentStream() {
const [status, setStatus] = useState<string>('');
const [toolCalls, setToolCalls] = useState<any[]>([]);
const [output, setOutput] = useState<string>('');
const [loading, setLoading] = useState(false);
const run = useCallback(async (slug: string, input: string, apiKey: string) => {
setLoading(true);
setStatus('Starting...');
setToolCalls([]);
setOutput('');
const response = await fetch(`https://api.aerostack.dev/api/run/${slug}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ input }),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = JSON.parse(line.slice(6));
if (data.type === 'thinking') setStatus(data.message);
if (data.type === 'tool_call') setToolCalls(prev => [...prev, data]);
if (data.type === 'tool_result') {
setToolCalls(prev => prev.map(tc =>
tc.tool === data.tool ? { ...tc, ...data } : tc
));
}
if (data.type === 'done') setOutput(data.output);
}
}
setLoading(false);
}, []);
return { run, status, toolCalls, output, loading };
}curl Example
curl -N -X POST https://api.aerostack.dev/api/run/summarizer \
-H "Authorization: Bearer aek_your_api_key" \
-H "Content-Type: application/json" \
-d '{"input": "Summarize this article..."}'The -N flag disables output buffering, so you see events as they arrive.
Response Headers
Streaming responses include these headers:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: noThe X-Accel-Buffering: no header prevents Nginx and similar proxies from buffering the stream.
JSON Output Format with Streaming
When output_format is set to json, token-level streaming is not available. The stream will emit thinking, tool_call, and tool_result events as usual, but the done event is emitted only after the complete JSON response is generated. This is because partial JSON cannot be validated against the output schema.
The done event for JSON output includes both output (stringified JSON) and outputParsed (the parsed object):
event: done
data: {"type":"done","output":"{\"summary\":\"...\",\"sentiment\":\"positive\"}","outputParsed":{"summary":"...","sentiment":"positive"},"tool_calls":[],"usage":{"tokens_input":245,"tokens_output":89,"cost_cents":0.12,"latency_ms":1823,"iterations":1}}Timeout Behavior
The agent execution engine enforces a hard timeout of 60 seconds (or the endpoint’s timeout_ms, whichever is lower). If the timeout is reached during a streaming session, the stream ends with whatever output has been produced so far. A thinking event with status: "timeout" is emitted before the stream closes.
Error Handling
If the execution engine encounters an error, an error event is emitted and the stream is closed:
event: error
data: {"type":"error","message":"Workspace not found or no active tokens."}Always handle the error event type in your client code to gracefully show errors to users.