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.
X-Request-ID— adopted (after sanitization) as the request id for log correlation and echoed back in theX-Request-IDresponse 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.
null values are omitted from each payload):
{"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 is401; 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.
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.
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.
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 fromnext_cursorof the previous page.
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.
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.
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 hasstatus: "pending_approval", use its
run_id and approval_id to decide or cancel the suspended run:
allow_once, deny, or allow_for_session:
deny. It succeeds only while the approval is pending and returns:
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.
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_idand is persisted with each message.
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.