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.

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.