Streaming with SSE
Stream updates from server to client with router.sse()
Server-Sent Events (SSE) provide efficient one-way streaming from server to client over HTTP. Use them for progress indicators, live feeds, notifications, and LLM response streaming.
Routes Location
All routes live in src/api/. Import agents you need and call them directly.
Basic Example
import { createRouter } from '@agentuity/runtime';
const router = createRouter();
router.sse('/updates', (c) => async (stream) => {
await stream.write('Connected!');
// Stream data to client
for (let i = 0; i < 5; i++) {
await stream.write(`Update ${i + 1}`);
await new Promise((r) => setTimeout(r, 1000));
}
stream.close();
});
export default router;Handler Structure
The SSE handler uses an async callback pattern:
router.sse('/path', (c) => async (stream) => {
// c - Route context (logger, agents, storage)
// stream - SSE stream object
await stream.write('data');
await stream.writeSSE({ event, data, id });
stream.onAbort(() => { /* cleanup */ });
stream.close();
});With Middleware
Apply authentication or logging before streaming:
import { createRouter } from '@agentuity/runtime';
import { createMiddleware } from 'hono/factory';
const router = createRouter();
const authMiddleware = createMiddleware(async (c, next) => {
const apiKey = c.req.header('X-API-Key');
if (!apiKey) {
return c.json({ error: 'API key required' }, 401);
}
await next();
});
router.sse('/events', authMiddleware, (c) => async (stream) => {
await stream.writeSSE({ event: 'connected', data: 'Authenticated!' });
// Stream events...
stream.close();
});
export default router;Two Write APIs
Simple Write
await stream.write('Hello');
await stream.write(JSON.stringify({ status: 'ok' }));Automatically formats data as SSE.
Full SSE Format
await stream.writeSSE({
event: 'status', // Event type for client filtering
data: 'Processing...', // The payload
id: '1', // Optional event ID
});Use this for named events that clients can filter.
Named Events
Clients can listen for specific event types:
Server:
await stream.writeSSE({ event: 'progress', data: '50%' });
await stream.writeSSE({ event: 'complete', data: JSON.stringify({ success: true }) });Client:
const source = new EventSource('/agent-name');
source.addEventListener('progress', (e) => {
console.log('Progress:', e.data);
});
source.addEventListener('complete', (e) => {
console.log('Done:', JSON.parse(e.data));
source.close();
});Full Example
A job progress tracker that streams status updates:
import { createRouter } from '@agentuity/runtime';
const router = createRouter();
router.sse('/', (c) => async (stream) => {
c.var.logger.info('Client connected');
const steps = [
'Loading resources...',
'Processing data...',
'Generating report...',
'Finalizing...',
];
let stepIndex = 0;
const interval = setInterval(async () => {
try {
if (stepIndex < steps.length) {
const progress = ((stepIndex + 1) / steps.length * 100).toFixed(0);
await stream.writeSSE({
event: 'status',
data: `[${progress}%] ${steps[stepIndex]}`,
id: String(stepIndex),
});
stepIndex++;
} else {
await stream.write(JSON.stringify({ success: true }));
clearInterval(interval);
stream.close();
}
} catch (error) {
c.var.logger.error('Stream error', { error });
clearInterval(interval);
}
}, 1000);
stream.onAbort(() => {
c.var.logger.info('Client disconnected');
clearInterval(interval);
});
// Keep connection open
await new Promise(() => {});
});
export default router;Client Disconnection
Handle early client disconnection with onAbort:
stream.onAbort(() => {
clearInterval(interval);
// Cancel any pending work
});Always clean up resources to prevent memory leaks.
Keeping the Connection Open
SSE connections stay open until closed. Use a pending promise to keep the handler alive:
router.sse('/stream', (c) => async (stream) => {
// Set up intervals, subscriptions, etc.
// Keep connection open until client disconnects or stream.close()
await new Promise(() => {});
});Client Connection
Connect from JavaScript using the EventSource API:
const source = new EventSource('https://your-project.agentuity.cloud/agent-name');
source.onmessage = (event) => {
console.log('Received:', event.data);
};
source.onerror = () => {
console.log('Connection error or closed');
source.close();
};Or with cURL:
curl -N https://your-project.agentuity.cloud/agent-nameSSE vs WebSocket
| Aspect | SSE | WebSocket |
|---|---|---|
| Direction | Server → Client only | Bidirectional |
| Protocol | HTTP | WebSocket |
| Reconnection | Built-in auto-reconnect | Manual |
| Browser support | Native EventSource | Native WebSocket |
| Best for | Progress, feeds, LLM streaming | Chat, collaboration |
Use SSE when you only need to push data from server to client. Use WebSockets when you need bidirectional communication.
Standalone Usage
SSE handlers work without agents. This example streams build logs from storage:
import { createRouter } from '@agentuity/runtime';
const router = createRouter();
router.sse('/builds/:id/logs', (c) => async (stream) => {
const buildId = c.req.param('id');
const logs = await c.var.kv.get<string[]>('builds', `${buildId}:logs`);
if (logs.exists) {
for (const line of logs.data) {
await stream.writeSSE({ event: 'log', data: line });
}
}
await stream.writeSSE({ event: 'complete', data: 'Build finished' });
stream.close();
});
export default router;Next Steps
- WebSockets: Bidirectional real-time communication
- HTTP Routes: Standard request/response endpoints
- React Hooks: Connect from React with
useEventStream
Need Help?
Join our Community for assistance or just to hang with other humans building agents.
Send us an email at hi@agentuity.com if you'd like to get in touch.
Please Follow us on
If you haven't already, please Signup for your free account now and start building your first agent!