Skip to content

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.

The SSE stream emits the following event types in order:

EventWhenPayload
thinkingAgent is processing{ status, message, iteration? }
tool_callAgent is invoking a tool{ iteration, tool, arguments }
tool_resultTool execution completed{ iteration, tool, success, latency_ms }
doneFinal output ready{ output, outputParsed?, tool_calls, usage }
errorExecution failed{ message }

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}}

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);
}
}
}
}
}

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;
}
}
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 };
}
Terminal window
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.

Streaming responses include these headers:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no

The X-Accel-Buffering: no header prevents Nginx and similar proxies from buffering the stream.

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}}

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.

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.