Build/Routes

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

SSE vs WebSocket

AspectSSEWebSocket
DirectionServer → Client onlyBidirectional
ProtocolHTTPWebSocket
ReconnectionBuilt-in auto-reconnectManual
Browser supportNative EventSourceNative WebSocket
Best forProgress, feeds, LLM streamingChat, 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

Need Help?

Join our DiscordCommunity 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!