HTTP & Web Frameworks
Agent Express provides a Web-standard Request -> Response handler that streams agent execution as Server-Sent Events. Import it from agent-express/http:
import { createHandler } from "agent-express/http"createHandler()
Section titled “createHandler()”Creates a handler function with the signature (req: Request) => Response. It uses the Web-standard Request/Response APIs, making it compatible with any framework that supports them.
import { Agent } from "agent-express"import { createHandler } from "agent-express/http"
const agent = new Agent({ name: "assistant", model: "anthropic/claude-sonnet-4-6", instructions: "You are a helpful assistant.",})
const handler = createHandler(agent)How It Works
Section titled “How It Works”- Accepts a POST request with JSON body
{ input: string } - Creates a session per request (or reuses one via session ID header)
- Runs one turn
- Streams
StreamEventobjects back as SSE (data: {...}\n\n) - Closes the session when done
HandlerOptions
Section titled “HandlerOptions”| Option | Type | Default | Description |
|---|---|---|---|
sessionIdHeader | string | "x-session-id" | Header name for session ID |
maxInputLength | number | 100000 | Maximum input string length in characters |
sessionTtlMs | number | 1800000 (30 min) | Idle session TTL — sessions are evicted after this period |
maxSessions | number | 10000 | Maximum concurrent sessions — rejects new sessions when full |
const handler = createHandler(agent, { sessionIdHeader: "x-my-session-id", maxInputLength: 50_000, sessionTtlMs: 60_000, // 1 minute TTL maxSessions: 1000,})Validation
Section titled “Validation”The handler validates:
- HTTP method is POST (returns 405 otherwise)
- Body has a string
inputfield (sends error event otherwise) - Session ID matches
[a-zA-Z0-9_-]{1,128}format (sends error event otherwise)
Error messages sent to clients are generic for security.
Request & Response Format
Section titled “Request & Response Format”Request Format
Section titled “Request Format”POST /api/agentContent-Type: application/jsonx-session-id: session-123 (optional)
{ "input": "Hello, how can you help?" }SSE Response Format
Section titled “SSE Response Format”The response is an SSE stream with Content-Type: text/event-stream:
data: {"type":"turn:start","turnIndex":0,"turnId":"abc-123"}
data: {"type":"model:start","model":"anthropic/claude-sonnet-4-6","callIndex":0}
data: {"type":"tool:start","tool":"lookup_order","args":{"orderId":"ORD-123"},"callId":"tc-1"}
data: {"type":"tool:end","tool":"lookup_order","result":{"orderId":"ORD-123","status":"shipped"}}
data: {"type":"model:start","model":"anthropic/claude-sonnet-4-6","callIndex":1}
data: {"type":"model:chunk","text":"Your order ORD-123 has been shipped"}
data: {"type":"model:end","finishReason":"stop"}
data: {"type":"turn:end","text":"Your order ORD-123 has been shipped and should arrive in 2 days."}
data: {"type":"session:end","result":{"text":"...","state":{...}}}StreamEvent Types
Section titled “StreamEvent Types”| Event | Fields | Description |
|---|---|---|
turn:start | turnIndex, turnId | Turn begins |
turn:end | text | Turn complete |
model:start | model, callIndex | LLM call begins |
model:chunk | text | Streamed text chunk |
model:end | finishReason | LLM call complete |
tool:start | tool, args, callId | Tool execution begins |
tool:end | tool, result | Tool execution complete |
session:end | result | Session complete with full RunResult |
error | error | Error occurred |
Express.js
Section titled “Express.js”Use toExpressHandler() to convert the Web-standard handler into an Express route handler:
import express from "express"import { Agent, tools } from "agent-express"import { createHandler, toExpressHandler } from "agent-express/http"import { z } from "zod"
const agent = new Agent({ name: "support", model: "anthropic/claude-sonnet-4-6", instructions: "You are a customer support agent.",}) .use(tools.function({ name: "lookup_order", description: "Look up an order by ID", schema: z.object({ orderId: z.string() }), execute: async ({ orderId }) => ({ orderId, status: "shipped", eta: "2 days" }), }))
const handler = createHandler(agent)const app = express()
app.post("/api/agent", toExpressHandler(handler))
app.listen(3000, () => { console.log("Agent API running on http://localhost:3000")})toExpressHandler() handles the conversion between Express req/res and Web Request/Response automatically.
Test With Curl
Section titled “Test With Curl”curl -X POST http://localhost:3000/api/agent \ -H "Content-Type: application/json" \ -d '{"input": "Where is my order ORD-123?"}' \ --no-bufferHono natively supports Web-standard Request/Response, making integration minimal.
import { Hono } from "hono"import { Agent, tools } from "agent-express"import { createHandler } from "agent-express/http"import { z } from "zod"
// 1. Create agentconst agent = new Agent({ name: "support", model: "anthropic/claude-sonnet-4-6", instructions: "You are a customer support agent.",}) .use(tools.function({ name: "lookup_order", description: "Look up an order by ID", schema: z.object({ orderId: z.string() }), execute: async ({ orderId }) => ({ orderId, status: "shipped", eta: "2 days" }), }))
// 2. Create handlerconst handler = createHandler(agent)
// 3. Mount routeconst app = new Hono()
app.post("/api/agent", (c) => handler(c.req.raw))
// 4. Start server (Node.js)import { serve } from "@hono/node-server"serve({ fetch: app.fetch, port: 3000 }, () => { console.log("Agent API running on http://localhost:3000")})Since Hono’s c.req.raw is already a Web Request, no conversion is needed. The handler returns a Web Response that Hono forwards directly.
Hono On Other Runtimes
Section titled “Hono On Other Runtimes”The same code works on Cloudflare Workers, Deno, and Bun:
// Cloudflare Workersexport default app
// Bunexport default { fetch: app.fetch, port: 3000 }
// DenoDeno.serve({ port: 3000 }, app.fetch)Fastify
Section titled “Fastify”Use toFastifyHandler() for Fastify integration:
import Fastify from "fastify"import { Agent, tools } from "agent-express"import { createHandler, toFastifyHandler } from "agent-express/http"import { z } from "zod"
const agent = new Agent({ name: "support", model: "anthropic/claude-sonnet-4-6", instructions: "You are a customer support agent.",}) .use(tools.function({ name: "lookup_order", description: "Look up an order by ID", schema: z.object({ orderId: z.string() }), execute: async ({ orderId }) => ({ orderId, status: "shipped", eta: "2 days" }), }))
const handler = createHandler(agent)const fastify = Fastify()
fastify.post("/api/agent", toFastifyHandler(handler))
fastify.listen({ port: 3000 }, () => { console.log("Agent API running on http://localhost:3000")})Session Management
Section titled “Session Management”By default, createHandler() creates a new ephemeral session per request. To maintain multi-turn conversations, pass a session ID via the x-session-id header. Sessions are stored in memory — conversation history and state persist across requests. For more on sessions, see Sessions.
# First request: creates sessioncurl -X POST http://localhost:3000/api/agent \ -H "Content-Type: application/json" \ -H "x-session-id: user-42" \ -d '{"input": "My name is Alice"}'
# Second request: same session, remembers contextcurl -X POST http://localhost:3000/api/agent \ -H "Content-Type: application/json" \ -H "x-session-id: user-42" \ -d '{"input": "What is my name?"}'# Response will include "Alice" — the session remembersSession Lifecycle
Section titled “Session Lifecycle”- No session ID: ephemeral session, created and closed per request
- With session ID: persistent session, kept in memory until TTL expires
- TTL eviction: idle sessions are automatically evicted (default 30 min)
- Max sessions: new sessions are rejected when the limit is reached (default 10K)
Session IDs must match [a-zA-Z0-9_-]{1,128}.
Security
Section titled “Security”Authentication and authorization are your responsibility. The handler does not verify session ownership — anyone with a valid session ID can access that session. Use your framework’s auth middleware to protect the endpoint:
// Express example with authapp.post("/api/agent", authMiddleware, toExpressHandler(handler))Client-Side Consumption
Section titled “Client-Side Consumption”Fetch With Streaming
Section titled “Fetch With Streaming”async function chat(input: string, sessionId?: string) { const headers: Record<string, string> = { "Content-Type": "application/json", } if (sessionId) headers["x-session-id"] = sessionId
const response = await fetch("/api/agent", { method: "POST", headers, body: JSON.stringify({ input }), })
const reader = response.body!.getReader() const decoder = new TextDecoder() let fullText = ""
while (true) { const { done, value } = await reader.read() if (done) break
const chunk = decoder.decode(value) for (const line of chunk.split("\n")) { if (!line.startsWith("data: ")) continue const event = JSON.parse(line.slice(6))
switch (event.type) { case "model:chunk": fullText += event.text updateUI(fullText) // Render streaming text break case "tool:start": showToolIndicator(event.tool) break case "session:end": onComplete(event.result) break } } }}