Skip to main content

Two APIs

Engine API

Stateless. Started by agentctl serve. Port 8090. Takes a message, returns a response. No session management.

Conversation API

Stateful. Started by agent-manager. Port 8100. Manages conversations, history, and SSE streaming. Also serves the widget.

Engine API

Base URL: http://localhost:8090

GET /health

POST /invoke

Send a message and get a response.
Response:
Two request headers are recognized:
  • X-Request-ID — adopted (after sanitization) as the request id for log correlation and echoed back in the X-Request-ID response header. Minted automatically when absent.
  • X-Session-ID — carried into the run as the conversation id for tracing metadata (e.g. the Langfuse session).

POST /stream

Same as /invoke but streams Server-Sent Events.
Events (fields with null values are omitted from each payload):
On failure a {"type": "error", "detail": "..."} event is emitted.

Conversation API

Base URL: http://localhost:8100 Manages multi-turn conversations with history persisted in SQLite (or Postgres via EXTRA_DB_URL).

Caller identity

Every conversation route requires a verified caller, and a conversation can only be listed, read, or written by the caller that owns it — the conversation id alone grants nothing. A request with no usable token is 401; a request from another user is 403. Identity arrives as a signed JWT, in either of two places:
  • Authorization: Bearer <token> — a token the host app minted, or a visitor pass the manager issued.
  • The host’s own session cookie, named by EXTRA_AUTH_COOKIE, when the manager is served from the host’s origin.
See Identity for how a host product is wired up, including the zero-code path and products with no login at all.
X-Agent-Chat-User was removed. It was an unverified header — anyone could send another user’s id and read their conversations.

POST /auth/anonymous

Issue a visitor pass: a signed token identifying one browser, for products with no login. The only route that does not require a caller.

POST /auth/link

Adopt the conversations a visitor started before signing in. Requires a verified caller; the body carries the visitor pass being handed over.
Linking happens once — a replayed pass returns 0. Errors: 401 the pass is not one of ours, 403 the caller is itself a visitor.

POST /conversations

Create a new conversation, owned by the calling user. The body is optional; a session_id may be supplied to give the conversation a stable, caller-chosen id. A supplied id is a name, not a claim. Re-creating an id you already own returns it unchanged, which keeps creation idempotent; asking for one owned by someone else returns 409 and never transfers ownership. A conversation’s owner is fixed at creation.
Response:
Errors: 409 the supplied session_id belongs to another user.

GET /conversations

List the calling user’s conversations, ordered by most recently active first, using keyset pagination. Query Parameters:
  • limit (integer, optional, default: 20, min: 1, max: 100) — Maximum number of conversations to return per page.
  • cursor (string, optional) — Opaque pagination cursor token obtained from next_cursor of the previous page.
Response:
When next_cursor is null, no further pages remain. Cursors are opaque server tokens and must not be constructed manually by clients.

POST /conversations/{id}/messages

Send a message. Prior history is assembled automatically.
Response:
Errors: 404 unknown conversation, 403 conversation owned by another user, 429 conversation token budget exceeded.

POST /conversations/{id}/messages/stream

Same as above but streams via SSE. Preferred by the widget. Each event carries an event: name matching its type; null fields are omitted.
Events:
On failure an event: error with {"type": "error", "error": "..."} is emitted before done.
The event schema also declares tool_started / tool_succeeded / tool_failed types for per-tool progress; the engine does not emit them yet.

GET /conversations/{id}/usage

Reports lifetime provider-reported token consumption against the configured conversation budget. Failed and cancelled turns and pending approvals remain charged; resumes update the same run’s total and completed answers are counted once. Editing away a branch does not refund its usage. New turns return 429 once the budget is exhausted, while existing approval continuations can finish. Usage observed during cancellation is retained after execution cleanup. Tokens the provider never reports cannot be included. The manager and engine must share their configured run repository to retain partial cancelled usage. Budget reads reconcile available run totals and retry missed retention writes, so a failed ledger write does not hide known usage from the budget guard. Pending-approval and final stream events, from both /messages/stream and /decision/stream, include the run’s cumulative input_tokens and output_tokens, so completed legs can also be accounted for from the event.

Pending approval actions

When a send or stream response has status: "pending_approval", use its run_id and approval_id to decide or cancel the suspended run:
The decision body is one of allow_once, deny, or allow_for_session:
Cancellation is a separate terminal lifecycle action, not an alias for deny. It succeeds only while the approval is pending and returns:
All routes authorize the conversation owner. If approval and cancellation race, exactly one wins; the loser receives 409. The widget uses /decision/stream. It returns SSE beginning with resume_started and the same run_id, followed by ordinary execution events. Disconnecting that response cancels the resumed graph task and moves the run to cancelled; it does not merely hide browser output.

GET /conversations/{id}/messages

Retrieve message history for a conversation.
Response:

Passing context to plugins

The HTTP layer currently forwards a deliberately small amount of caller context into the run:
  • X-Session-ID (Engine API) becomes the run’s conversation id, visible to tracing.
  • The verified caller (Conversation API) becomes run_context.user_id and is persisted with each message.
Populating run_context.auth_context (roles, organization, custom claims) from the verified token is the next step — see the roadmap. Hooks such as on_run_start can inject a modified RunContext in the meantime.