Server API Reference

Complete protocol reference for the noumen agent server — REST endpoints, SSE streaming, WebSocket messages, middleware adapter, and headless CLI.

noumen ships a built-in server (noumen/server) that exposes the agent over HTTP/SSE and WebSocket, plus a headless CLI mode for subprocess control. This page documents the full protocol.

Standalone server

import { Agent, AiSdkProvider } from "noumen";
import { LocalSandbox } from "noumen/local";
import { createServer } from "noumen/server";
import { createAnthropic } from "@ai-sdk/anthropic";

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const code = new Agent({
  provider: new AiSdkProvider({
    model: anthropic("claude-opus-4.6"),
    providerFamily: "anthropic",
  }),
  sandbox: LocalSandbox({ cwd: "/workspace" }),
  options: { permissions: { mode: "bypassPermissions" } },
});

await code.init();

const server = createServer(code, {
  port: 3001,
  auth: { type: "bearer", token: process.env.API_TOKEN! },
  cors: true,
  ws: true,
  maxSessions: 10,
  idleTimeoutMs: 300_000,
  pendingTimeoutMs: 120_000,
});

await server.start();

ServerOptions

OptionTypeDefaultDescription
portnumberrequiredTCP port to listen on
hoststring"0.0.0.0"Bind address
wsbooleantrueEnable WebSocket transport (requires ws package)
authAuthConfignoneBearer token or custom auth function
maxSessionsnumberunlimitedMaximum concurrent sessions
idleTimeoutMsnumbernoneReap sessions idle longer than this
corsbooleantrueSet CORS headers for browser clients
pendingTimeoutMsnumber120000Timeout for pending permission/input responses
onConnectionfunctionnoneCalled per connection; return { cwd } overrides
onErrorfunctionnoneError callback

AuthConfig

// Bearer token
{ type: "bearer", token: "my-secret" }

// Custom verifier
{ type: "custom", verify: (req) => req.headers["x-api-key"] === "..." ? {} : null }

REST endpoints

All endpoints except /health require authentication when auth is configured.

GET /health

Health check. Does not require auth.

Response 200:

{ "status": "ok", "sessions": 2 }

POST /sessions

Create a session and start an agent run.

Body:

{ "prompt": "Fix the failing test", "sessionId": "optional-custom-id" }

Response 201:

{ "sessionId": "abc-123", "eventsUrl": "/sessions/abc-123/events" }

Errors: 400 missing prompt, 429 max sessions reached.

GET /sessions

List active sessions.

Response 200:

[{ "id": "abc-123", "lastActivity": 1712188800000, "done": false }]

GET /sessions/:id/events

SSE event stream. Connect after creating a session to receive events.

Headers: Content-Type: text/event-stream

Each event has an incrementing id: for resumption:

id: 1
data: {"type":"text_delta","text":"Hello"}

id: 2
data: {"type":"tool_use_start","toolName":"ReadFile","toolUseId":"call_abc123"}

id: 3
data: {"type":"turn_complete","usage":{"prompt_tokens":100,"completion_tokens":50,"total_tokens":150},"model":"gpt-4o","callCount":1}

Resumption: On reconnect, send Last-Event-ID: 3 to skip already-received events.

Keepalive: The server sends :keepalive comments every 15 seconds.

Subscriber replacement: If a second client connects to the same session's event stream, the first receives {"type":"subscriber_replaced"} and is disconnected.

POST /sessions/:id/permissions

Resolve a pending permission request.

Body:

{ "allow": true }

Errors: 404 session not found, 409 no pending permission request.

POST /sessions/:id/input

Resolve a pending user input request.

Body:

{ "answer": "yes, deploy to production" }

Errors: 404 session not found, 409 no pending input request.

POST /sessions/:id/messages

Send a follow-up prompt to an idle session.

Body:

{ "prompt": "Now run the tests" }

Errors: 404 session not found, 409 session is still running.

DELETE /sessions/:id

Abort and destroy a session.

Response 200:

{ "ok": true }

WebSocket protocol

Connect to ws://host:port (or wss:// for TLS). The same HTTP server handles both REST and WebSocket.

Authentication

  • Query parameter: ws://host:port?token=my-secret
  • Authorization header: Standard Authorization: Bearer my-secret on the upgrade request

Client-to-server messages

run

Start a new agent session.

{ "type": "run", "prompt": "Fix the bug", "sessionId": "optional-id" }

Server responds with session_created, then streams events.

message

Send a follow-up prompt to an idle session.

{ "type": "message", "sessionId": "abc-123", "prompt": "Now add tests" }

permission_response

Resolve a pending permission request.

{ "type": "permission_response", "sessionId": "abc-123", "allow": true }

input_response

Resolve a pending user input request.

{ "type": "input_response", "sessionId": "abc-123", "answer": "yes" }

abort

Abort and destroy a session.

{ "type": "abort", "sessionId": "abc-123" }

Server-to-client messages

All messages include sessionId and seq (incrementing sequence number):

{ "type": "session_created", "sessionId": "abc-123" }
{ "type": "text_delta", "text": "Hello", "sessionId": "abc-123", "seq": 1 }
{ "type": "turn_complete", "sessionId": "abc-123", "seq": 5, "usage": { ... } }
{ "type": "error", "sessionId": "abc-123", "error": "Something went wrong" }

Connection lifecycle

  • Ping/pong: Server pings every 30 seconds; closes connections that fail to respond.
  • Session cleanup: When the WebSocket closes, all sessions created on that connection are destroyed.

Middleware adapter

Mount the agent on an existing HTTP server using createRequestHandler():

import express from "express";
import { Agent, AiSdkProvider } from "noumen";
import { LocalSandbox } from "noumen/local";
import { createRequestHandler } from "noumen/server";
import { createOpenAI } from "@ai-sdk/openai";

const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY! });

const code = new Agent({
  provider: new AiSdkProvider({ model: openai.chat("gpt-5") }),
  sandbox: LocalSandbox({ cwd: "/workspace" }),
  options: { permissions: { mode: "bypassPermissions" } },
});
await code.init();

const app = express();
app.use("/agent", createRequestHandler(code, {
  auth: { type: "bearer", token: process.env.API_TOKEN! },
  cors: true,
  maxSessions: 5,
}));
app.listen(3000);

The handler supports all REST endpoints documented above. Routes are relative to the mount path (e.g. POST /agent/sessions).

WebSocket is not supported in middleware mode. Use createServer() for WebSocket support.

RequestHandlerOptions

Same as ServerOptions but without port, host, and ws.


Headless CLI protocol

Run noumen --headless to start the agent in headless mode. Communication is bidirectional NDJSON over stdin/stdout.

noumen --headless -p anthropic -m claude-sonnet-4

Startup

The process emits {"type":"ready"} on stdout when initialization is complete.

Inbound commands (stdin)

One JSON object per line:

prompt

{"type":"prompt","text":"Fix the failing test","sessionId":"optional-id","maxTurns":10}

permission_response

{"type":"permission_response","sessionId":"abc-123","allow":true}

input_response

{"type":"input_response","sessionId":"abc-123","answer":"yes"}

abort

{"type":"abort","sessionId":"abc-123"}

Outbound events (stdout)

One JSON object per line. All stream events include a sessionId field:

{"type":"ready"}
{"type":"session_created","sessionId":"abc-123"}
{"type":"text_delta","text":"Hello","sessionId":"abc-123"}
{"type":"tool_use_start","toolName":"ReadFile","input":{"path":"src/main.ts"},"sessionId":"abc-123"}
{"type":"turn_complete","sessionId":"abc-123"}
{"type":"session_done","sessionId":"abc-123"}

Usage from Node.js

import { spawn } from "node:child_process";

const agent = spawn("npx", ["noumen", "--headless", "-p", "anthropic"], {
  stdio: ["pipe", "pipe", "inherit"],
});

const rl = readline.createInterface({ input: agent.stdout });

for await (const line of rl) {
  const event = JSON.parse(line);
  if (event.type === "ready") {
    agent.stdin.write(JSON.stringify({ type: "prompt", text: "Hello" }) + "\n");
  }
  if (event.type === "text_delta") {
    process.stdout.write(event.text);
  }
  if (event.type === "session_done") break;
}

agent.kill();

Usage from Python

import subprocess, json

proc = subprocess.Popen(
    ["npx", "noumen", "--headless", "-p", "anthropic"],
    stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None,
    text=True, bufsize=1,
)

for line in proc.stdout:
    event = json.loads(line)
    if event["type"] == "ready":
        proc.stdin.write(json.dumps({"type": "prompt", "text": "Hello"}) + "\n")
        proc.stdin.flush()
    if event["type"] == "text_delta":
        print(event["text"], end="")
    if event["type"] == "session_done":
        break

proc.terminate()

Stream event types

For a full reference of all StreamEvent types emitted by the agent, see Stream Events.

The most common events to handle:

EventDescription
text_deltaModel streaming text (field: text)
thinking_deltaModel thinking/reasoning (field: text)
tool_use_startTool call begins (fields: toolName, toolUseId)
tool_resultTool call result (field: result)
permission_requestAgent needs permission (fields: toolName, input)
turn_completeAgent turn finished (field: usage)
errorError occurred (field: error)