Router and Routes — Agentuity Documentation

Router and Routes

HTTP endpoints, middleware, WebSocket, SSE, and cron handlers

The SDK uses Hono for HTTP routes. Define routes in src/api/index.ts, mount them from your root app.ts, and export the router type when you want typed clients with hc().

Creating Routes

Create routes with createRouter() from @agentuity/runtime (which returns new Hono<Env>() with the correct environment type), or use new Hono<Env>() directly. Both work the same way for hc() type inference.

Basic Setup:

// src/api/index.ts
import { createRouter } from '@agentuity/runtime';
 
const router = createRouter()
  .get('/', (c) => {
    return c.json({ message: 'Hello from route' });
  });
 
export type ApiRouter = typeof router;
 
export default router;

Router Context

Router handlers receive a context parameter (typically c) that provides access to the request, response helpers, and Agentuity services. This Hono Context is distinct from the AgentContext type used in agent handlers.

Understanding Context Types

Agentuity uses two distinct context types based on where you're writing code:

  • AgentContext: Used in agent.ts files for business logic (no HTTP access)
  • Router Context (Hono): Used in src/api/index.ts for HTTP handling (has HTTP + agent services)

Route examples typically use c, while agent examples typically use ctx. The distinction is still type-based, not name-based.

Both contexts expose the same built-in services, but through different access patterns.

Router Context Interface:

interface RouterContext<TAppState = Record<string, never>> {
  // Request
  req: HonoRequest;                  // Hono request object with .param(), .query(), .header(), .json()
 
  // Agentuity Services (via c.var)
  var: {
    sessionId: string;                // Request/session identifier
    kv: KeyValueStorage;              // Key-value storage
    vector: VectorStorage;            // Vector storage
    stream: StreamStorage;            // Stream storage
    queue: QueueService;              // Message queues
    task: TaskStorage;                // Task tracking
    email: EmailService;              // Email sending and receiving
    schedule: ScheduleService;        // Scheduled tasks
    sandbox: SandboxService;          // Isolated code execution
    logger: Logger;                   // Structured logging
    tracer: Tracer;                   // OpenTelemetry tracing
    meter: Meter;                     // OpenTelemetry metrics
    thread: Thread;                   // Thread context
    session: Session;                 // Session context
    app: TAppState;                   // App-level state from createApp()
  };
 
  waitUntil(callback: Promise<void> | (() => void | Promise<void>)): void;
 
  // Response Helpers
  json(data: unknown, status?: number): Response;
  text(text: string, status?: number): Response;
  html(html: string, status?: number): Response;
  redirect(url: string, status?: number): Response;
  // ... other Hono response methods
}

Key Differences Between Context Types:

FeatureRouter Context (Hono)Agent Context
TypeHono ContextAgentContext
Used insrc/api/index.tsagent.ts files
Request accessc.req (Hono Request)Direct input parameter (validated)
ResponseBuilder methods (.json(), .text())Direct returns
Servicesc.var.kv, c.var.logger, etc.ctx.kv, ctx.logger, etc.
Agent callingImport and call: agent.run()Import and call: agent.run()
State managementc.var.thread, c.var.session, c.var.appctx.state, ctx.thread, ctx.session, ctx.app

Example Usage:

import processor from '@agent/processor/agent';
 
router.post('/process', processor.validator(), async (c) => {
  // Access request
  const body = c.req.valid('json');
  const authHeader = c.req.header('Authorization');
 
  // Use Agentuity services
  c.var.logger.info('Processing request', { body });
 
  // Call an agent
  const result = await processor.run({ data: body.data });
 
  // Store result
  await c.var.kv.set('results', body.id, result);
 
  // Return response
  return c.json({ success: true, result });
});

Accessing Services

Agentuity services (storage, logging, tracing) are available in multiple contexts. The API is identical; only the access pattern differs.

Quick Reference

ServiceIn AgentsIn RoutesIn Standalone
Key-Valuectx.kvc.var.kvctx.kv
Vectorctx.vectorc.var.vectorctx.vector
Streamsctx.streamc.var.streamctx.stream
Queuectx.queuec.var.queuectx.queue
Tasksctx.taskc.var.taskctx.task
Emailctx.emailc.var.emailctx.email
Schedulectx.schedulec.var.schedulectx.schedule
Sandboxctx.sandboxc.var.sandboxctx.sandbox
Loggerctx.loggerc.var.loggerctx.logger
Tracerctx.tracerc.var.tracerctx.tracer
MeterN/Ac.var.meterN/A
Statectx.stateN/Actx.state
Threadctx.threadc.var.threadctx.thread
Sessionctx.sessionc.var.sessionctx.session
App statectx.appc.var.appctx.app

From Agents

import { createAgent } from '@agentuity/runtime';
 
export default createAgent('cache-manager', {
  handler: async (ctx) => {
    await ctx.kv.set('cache', 'key', { data: 'value' });
    ctx.logger.info('Data cached');
    return { success: true };
  },
});

From Routes

import { Hono } from 'hono';
import type { Env } from '@agentuity/runtime';
 
const router = new Hono<Env>();
 
router.post('/cache', async (c) => {
  await c.var.kv.set('cache', 'key', { data: 'value' });
  c.var.logger.info('Data cached');
  return c.json({ success: true });
});
 
export default router;

From Standalone Code

For Discord bots, CLI tools, or queue workers using the Agentuity runtime services:

import { createAgentContext } from '@agentuity/runtime';
import cleanupAgent from '@agent/cleanup/agent';
 
const ctx = createAgentContext({ trigger: 'manual' });
 
await ctx.invoke(async () => {
  await ctx.kv.set('cache', 'key', { data: 'value' });
  ctx.logger.info('Data cached from standalone context');
});
 
await ctx.run(cleanupAgent, { namespace: 'cache' });

See Running Agents Without HTTP for Discord bots, CLI tools, and queue worker patterns.

From External Backends (Next.js, Express)

External backends cannot access Agentuity services directly. Create authenticated routes that expose storage operations, then call them via HTTP:

typescriptAgentuity route: src/api/sessions/route.ts
import { Hono } from 'hono';
import type { Env } from '@agentuity/runtime';
 
const router = new Hono<Env>();
 
router.get('/:id', async (c) => {
  const result = await c.var.kv.get('sessions', c.req.param('id'));
  return result.exists ? c.json(result.data) : c.json({ error: 'Not found' }, 404);
});
 
export default router;
typescriptNext.js: lib/agentuity.ts
const AGENTUITY_URL = process.env.AGENTUITY_URL!;
const API_KEY = process.env.STORAGE_API_KEY!;
 
export async function getSession(id: string) {
  const res = await fetch(`${AGENTUITY_URL}/api/sessions/${id}`, {
    headers: { 'x-api-key': API_KEY },
  });
  if (!res.ok) return null;
  return res.json();
}

See SDK Utilities for External Apps for the complete pattern with authentication.

From Frontend

Frontend code reaches Agentuity services through routes. For type-safe clients, export ApiRouter and use hc() from hono/client:

import { useEffect, useState } from 'react';
import { hc } from 'hono/client';
import type { ApiRouter } from '../api/index';
 
const client = hc<ApiRouter>('/api');
 
function SessionView({ id }: { id: string }) {
  const [data, setData] = useState<{ message: string } | null>(null);
 
  useEffect(() => {
    client.sessions[':id'].$get({ param: { id } })
      .then((res) => res.ok ? res.json() : null)
      .then(setData);
  }, [id]);
 
  if (!data) return <div>Loading...</div>;
  return <div>{data.message}</div>;
}

See RPC Client for hc() patterns in React, Next.js, and other frontend environments.

HTTP Methods

The router supports all standard HTTP methods.

GET Requests:

router.get('/users', (c) => {
  return c.json({ users: [] });
});
 
router.get('/users/:id', (c) => {
  const id = c.req.param('id');
  return c.json({ userId: id });
});

POST Requests:

router.post('/users', async (c) => {
  const body = await c.req.json();
  return c.json({ created: true, user: body });
});

PUT, PATCH, DELETE:

router.put('/users/:id', async (c) => {
  const id = c.req.param('id');
  const body = await c.req.json();
  return c.json({ updated: true, id, data: body });
});
 
router.patch('/users/:id', async (c) => {
  const id = c.req.param('id');
  const updates = await c.req.json();
  return c.json({ patched: true, id, updates });
});
 
router.delete('/users/:id', (c) => {
  const id = c.req.param('id');
  return c.json({ deleted: true, id });
});

Calling Agents from Routes:

Import agents and call them directly:

import processorAgent from '@agent/processor/agent';
 
router.post('/process', processorAgent.validator(), async (c) => {
  const input = c.req.valid('json');
 
  // Call the agent
  const result = await processorAgent.run({
    data: input.data,
  });
 
  return c.json(result);
});

Specialized Routes

The router provides specialized route handlers for non-HTTP triggers like WebSockets, scheduled jobs, and real-time communication.

WebSocket Routes

Create a WebSocket endpoint for real-time bidirectional communication using the websocket middleware.

Import:

import { websocket } from '@agentuity/runtime';

Handler Signature:

type WebSocketHandler = (c: Context, ws: WebSocketConnection) => void;
 
interface WebSocketConnection {
  onOpen(handler: (event: Event) => void | Promise<void>): void;
  onMessage(handler: (event: MessageEvent) => void | Promise<void>): void;
  onClose(handler: (event: CloseEvent) => void | Promise<void>): void;
  send(data: string | ArrayBuffer | Uint8Array): void;
}

The websocket() handler itself must be synchronous. Put async work inside onOpen, onMessage, or onClose callbacks.

Example:

import { Hono } from 'hono';
import type { Env } from '@agentuity/runtime';
import { websocket } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
import chatAgent from '@agent/chat/agent';
 
const router = new Hono<Env>();
const messageSchema = s.object({ text: s.string() });
 
router.get('/chat', websocket((c, ws) => {
  ws.onOpen(() => {
    c.var.logger.info('WebSocket connected');
    ws.send(JSON.stringify({ type: 'connected' }));
  });
 
  ws.onMessage(async (event) => {
    if (typeof event.data !== 'string') {
      c.var.logger.warn('Ignoring non-text WebSocket message');
      return;
    }
 
    const raw: unknown = JSON.parse(event.data);
    const message = messageSchema.parse(raw);
 
    // Process message with agent
    const response = await chatAgent.run({
      message: message.text,
    });
 
    ws.send(JSON.stringify({ type: 'response', data: response }));
  });
 
  ws.onClose(() => {
    c.var.logger.info('WebSocket disconnected');
  });
}));
 
export default router;

Server-Sent Events (SSE)

Create a Server-Sent Events endpoint for server-to-client streaming using the sse middleware.

Import:

import { sse } from '@agentuity/runtime';

Handler Signature:

type SSEHandler = (c: Context, stream: SSEStream) => void | Promise<void>;
 
interface SSEStream {
  write(data: string | number | boolean | SSEMessage): Promise<void>;
  writeSSE(message: SSEMessage): Promise<void>;
  onAbort(handler: () => void): void;
  close(): void;
}
 
interface SSEMessage {
  data: string;
  event?: string;
  id?: string;
  retry?: number;
}

Use sse({ output: schema }, handler) when you want generated route types for SSE event data. The schema is used for type inference, not runtime validation of each event.

Example:

import { Hono } from 'hono';
import type { Env } from '@agentuity/runtime';
import { sse } from '@agentuity/runtime';
import longRunningAgent from '@agent/long-running/agent';
 
const router = new Hono<Env>();
 
router.get('/updates', sse(async (c, stream) => {
  // Send initial connection message
  await stream.writeSSE({
    event: 'connected',
    data: JSON.stringify({ type: 'connected' }),
  });
 
  // Stream agent progress updates
  const updates = await longRunningAgent.run({ task: 'process' });
 
  for (const update of updates) {
    await stream.writeSSE({
      event: 'progress',
      data: JSON.stringify({ type: 'progress', data: update }),
    });
  }
 
  // Clean up on client disconnect
  stream.onAbort(() => {
    c.var.logger.info('Client disconnected');
  });
}));
 
export default router;

Stream Routes

Create an HTTP streaming endpoint for piping data streams using the stream middleware.

Import:

import { stream } from '@agentuity/runtime';

Handler Signature:

type StreamHandler = (
  c: Context
) => ReadableStream<Uint8Array | string> | Promise<ReadableStream<Uint8Array | string>>;

Example:

import { Hono } from 'hono';
import type { Env } from '@agentuity/runtime';
import { stream } from '@agentuity/runtime';
import dataGenerator from '@agent/data-generator/agent';
 
const router = new Hono<Env>();
 
router.post('/data', stream(async (c) => {
  // Create a readable stream
  const readableStream = new ReadableStream<Uint8Array>({
    async start(controller) {
      // Stream data chunks
      const data = await dataGenerator.run({ query: 'all' });
 
      for (const chunk of data) {
        controller.enqueue(new TextEncoder().encode(JSON.stringify(chunk) + '\n'));
      }
 
      controller.close();
    },
  });
 
  return readableStream;
}));
 
export default router;

For streaming agent outputs, see Streaming Responses.

Cron Routes

Schedule recurring jobs using cron syntax with the cron middleware.

Import:

import { cron } from '@agentuity/runtime';

Handler Signature:

type CronHandler = (c: Context) => unknown | Promise<unknown>;

Cron Schedule Format:

┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6) (Sunday = 0)
│ │ │ │ │
* * * * *

Example:

import { Hono } from 'hono';
import type { Env } from '@agentuity/runtime';
import { cron } from '@agentuity/runtime';
import reportGenerator from '@agent/report-generator/agent';
import healthCheck from '@agent/health-check/agent';
 
const router = new Hono<Env>();
 
// Run daily at 9am
router.post('/daily-report', cron('0 9 * * *', { auth: true }, async (c) => {
  c.var.logger.info('Running daily report');
 
  const report = await reportGenerator.run({
    type: 'daily',
    date: new Date().toISOString(),
  });
 
  // Store report in KV
  await c.var.kv.set('reports', `daily-${Date.now()}`, report);
 
  return c.json({ success: true });
}));
 
// Run every 5 minutes
router.post('/health-check', cron('*/5 * * * *', { auth: true }, async (c) => {
  await healthCheck.run({});
  return c.json({ checked: true });
}));
 
export default router;

For platform-managed schedules with delivery tracking and retry, see Schedules.

Route Parameters

Access route parameters and query strings through the request object.

Path Parameters:

router.get('/posts/:postId/comments/:commentId', (c) => {
  const postId = c.req.param('postId');
  const commentId = c.req.param('commentId');
 
  return c.json({ postId, commentId });
});

Query Parameters:

router.get('/search', (c) => {
  const query = c.req.query('q');
  const limit = c.req.query('limit') || '10';
  const page = c.req.query('page') || '1';
 
  return c.json({
    query,
    limit: Number.parseInt(limit, 10),
    page: Number.parseInt(page, 10),
  });
});

Request Headers:

router.get('/protected', (c) => {
  const authHeader = c.req.header('Authorization');
 
  if (!authHeader) {
    return c.json({ error: 'Unauthorized' }, 401);
  }
 
  return c.json({ authorized: true });
});