Identity is server-derived. Public clients reach these routes through the gateway under the
canonical prefix
/agent/*. The gateway resolves your
X-API-Key → user and injects X-User-Id; agent-api derives the workspace/chat/quota subject from
that header (P20). A subject in a request body or query string is ignored — never trust the client
for identity. The $AGENT_API/api/... paths below address the internal service directly (which requires
X-User-Id); through the gateway the same routes are $API_BASE/agent/... with X-API-Key.Health
GET /health
Models
GET /api/models → the resolved model names for this subject:
Dispatch a unit
POST /invocations — the sink every trigger funnels through. Body is a conformant unit.v1 Invocation
(the same envelope units.make_dispatch builds); it is validated and dispatched to the runtime. 400 if
the envelope is non-conformant.
POST /invocations → 202
identity.subject + identity.launcher, runner, a workspaces list (mode is
rw for trusted triggers — message/scheduled — else ro), the trigger, and a start that is
either {"entrypoint":{"inline":"..."}}, {"entrypoint":{"path":"..."}}, or {"session":{"ref":"..."}}.
There is no top-level subject, workspace_repo, or plan field.
Chat (streamed)
POST /api/chat — body { prompt, session?, active? } (subject is server-derived, not sent).
Returns Server-Sent Events; each data: line is one frame:
| frame | fields |
|---|---|
message-delta | text |
tool-call | tool, args, callId |
tool-result | callId, ok, summary |
commit | sha |
rejected | violations |
done | reply, sessionId, ok |
POST /api/chat (SSE)
POST /api/chat/reset { session? } → { "ok": true } · GET /api/sessions →
{ "sessions": [...] } · GET /api/sessions/{session}/history → { "turns": [...] } (tolerant — a
missing transcript returns { "turns": [] }).
Routines
POST /api/routines → 201
GET /api/routines → { "routines": [...] } ·
PATCH /api/routines/{name}/enabled { enabled } → { "ok": true, "name", "enabled", "reconcile" } ·
DELETE /api/routines/{routine_id} → { "ok": true, "routine_id": "..." }.
Events
POST /events — body is an event.v1 Event (an integration firing). It maps to a unit.v1 Invocation
and dispatches. 400 if non-conformant; 422 if the event carries no plan.
POST /events → 202
Live meeting copilot
A live-meeting copilot is itself driven throughmake_dispatch; the id invariant is
meeting_id == session_uid == native_id.
POST /api/meeting/start{ native_id, platform, title? }→202, returns the live-meeting record.POST /api/meeting/process{ native_id, platform, on }→ toggle opt-in processing.on:trueresumes from the per-meeting cursor ({ ..., "processing":true, "resumed_from":"<id>" });on:falseclears the flag and freezes the cursor ({ ..., "processing":false }).GET /api/meeting/relay-health→ typed transcript-relay health (a stale bot key surfaces asnative_resolve:{ok:false,kind:"unauthorized"}rather than silent dead air).GET /api/meeting/stream?meeting_id=&session_uid=(SSE) → the merged transcript + copilot-output feed. Resumable: every event carries an SSEid:; on reconnect echo it asLast-Event-IDto resume gaplessly. Event types:transcript,card,message-delta,tool-call,ping,meeting-end.
Workspace
GET /api/workspace/tree?hidden= → { "files": ["kg/entities/..."] } ·
GET /api/workspace/file?path= → { "path": "...", "content": "..." } (404 if absent) ·
GET /api/workspace/git → { branch, changes, commits } ·
POST /api/workspace/upload (multipart files, ≤25 MB each) → { "files": [{ name, path }] } ·
POST /api/workspace/init → 201 { workspace, seeded, already_initialized } (idempotent) ·
GET /api/workspace/attached → { active, parked } ·
POST /api/workspace/swap { repo?, ref?, token? } → attach an external git repo as the active
workspace (parks the current one; omit repo to swap back to the seed).
(All workspace routes derive subject from X-User-Id — no subject query parameter.)