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.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.