Skip to content

Operator REST API ​

The console drives the backend over a REST control-plane under /api/* (defined in operator_api/). This is the operator surface — managing one agent and its host. For talking to the agent as a client, use the A2A endpoints (/a2a) or the OpenAI-compatible /v1 surface instead.

All /api/* routes are gated by the same bearer auth as the rest of the server (set via A2A_AUTH_TOKEN / the configured token); the console attaches it automatically. This page is a map — operator_api/*.py is the source of truth for exact request/response shapes.

Error shapes ​

Every error body is {"detail": …}, in one of three shapes:

ShapeWhen
detail: "<message>" (a string)Most routes. 400 = the request's input was refused (the message says why), 404 = the named resource doesn't exist, 409 = the subsystem isn't loaded yet (setup incomplete), 503 = the feature isn't enabled. A 500 is a fault in the server: its detail is a generic message with a short error id (Internal server error (error id 1a2b3c4d); …) — the real exception is in the server log under that id, never in the response.
detail: [{loc, msg, type, …}, …] (a list)422 — the body or query failed type validation before the route ran (FastAPI's standard shape).
detail: {code, message, upstream_status, session_id, error_id} (an object)POST /api/chat when the turn failed — see below — and POST /api/subagents/run / /batch when the model provider failed (429 / 502). A few other routes use an object {code, reason} (noted per route).

A client should read detail as: a string → show it; an object → show message (or reason); a list → show the first item's msg.

A 502 with a string detail comes from the fleet hub's proxy (the agent isn't up or reachable yet) and is worth retrying; a 502 with an object detail is the agent itself reporting that its model provider failed, so retrying right away won't help. The console tells the two apart this way.

Runtime & health ​

MethodPathPurpose
GET/healthzLiveness probe
GET/api/runtime/statusSetup state, model, enabled middleware, knowledge/scheduler/skills counts, code_pane: {enabled} (the console gates the code pane on it)

Chat & sessions ​

MethodPathPurpose
POST/api/chatRun a non-streaming chat turn (the streaming path is A2A /a2a). Success → {response, messages, session_id}. A failed turn is a real HTTP error with the same status policy as /v1: 429 when the provider rate-limited (with Retry-After when it sent one), 502 for any other upstream failure, an unreachable gateway, or a provider that closed the stream, 503 when the turn's model connection can't be set up right now (Retry-After), 400 when the request named an unknown model, 500 otherwise. The body is {"detail": {code, message, upstream_status, session_id, error_id}}: message is the turn's own user-facing reason (the same text the transcript records); only when the server itself crashed is it replaced by a generic message carrying error_id. error_id is always logged next to the real message.
GET/api/chat/sessions/{id}One session + its busy signal: {session_id, active, turn_count, last_updated, last_state}. active is true while a turn is running on the session from any surface (console, A2A, /api/chat); the Zed shim polls it before sending so two turns never interleave. Unknown or deleted → 404 {detail: {code: "not_found"}}
DELETE/api/chat/sessions/{id}Delete a session (?harvest=true to extract memory first; ?forget=true to remove what it already wrote to memory: its compaction archives and harvested summaries/facts; ?retire=false clears it but keeps the id)
GET/api/chat/commandsSlash-command inventory (workflows / subagents / skills)
POST/api/chat/sessions/{id}/steerEnqueue a mid-turn steering message
GET/api/chat/sessions/{id}/steerPeek pending steers
DELETE/api/chat/sessions/{id}/steer/{msg_id}Cancel a queued steer
GET/api/chat/sessions/{id}/delegationsList running subagent delegations
POST/api/chat/sessions/{id}/delegations/{del_id}/cancelCancel one delegation (lead continues)

Goals (goal mode) ​

MethodPathPurpose
GET/api/goalsList goals across sessions
GET/api/goals/{session_id}One goal's detail — status + its durable plan artifact (plan, the .plan.md the agent maintains via update_goal_plan, ADR 0079)
POST/api/goalsSet a goal: {session_id, condition, verifier?, max_iterations?, no_progress_limit?, outcome?, constraints?, boundaries?, stop_when?, kick?}. Optional completion-contract fields (ADR 0073) + kick (default true; the console panel sends false and drives the goal from a dedicated chat tab instead of a headless turn). max_iterations is a whole number 1..1000 and no_progress_limit 1..100 (omit either for the config default); a wrong type or out-of-range value is a 422. A refused goal (missing condition, unknown verifier) is a 400
POST/api/goals/{session_id}/rearmRe-arm: extend an active goal's iteration budget (add_iterations), or reactivate a terminal one and kick a fresh drive turn
POST/api/goals/{session_id}/resumeKick a headless continuation for an active goal (used when a chat tab driving it is closed but the goal is kept running)
DELETE/api/goals/{session_id}Clear (stop) a goal. ?close_tasks=true also closes the goal's session-scoped task backlog (ADR 0079)

Subagents & tools ​

MethodPathPurpose
GET/api/subagentsRegistered subagents (allowlists, max turns)
POST/api/subagents/runRun one subagent manually. A model-provider failure is 429 (mirrored, with Retry-After when the provider sent one) or 502 (any other upstream failure or an unreachable gateway), with the object detail {code, message, upstream_status, session_id, error_id}; the same applies to /batch
POST/api/subagents/batchRun several subagents concurrently: {session_id?, tasks: [{prompt, description?, type?, subagent_type?}]}, at most 20 tasks. Every task needs a non-empty prompt — one task without it fails the whole batch with 422 (it used to run the others and report that task as an error)
GET/api/toolsWired tools (core / plugin / MCP)
GET/api/acp-agentsDetected ACP coding agents

Background jobs & scheduler ​

MethodPathPurpose
GET/api/backgroundBackground subagent jobs. ?status= must be one of running, completed, failed, canceled (else 400)
GET/api/background/{job_id}One job's full row by id (full result text; ADR 0070)
POST/api/background/{job_id}/cancel · /api/background/clearCancel one / clear finished
DELETE/api/background/{job_id}Remove a job row. Every /api/background/{job_id} route checks the id is bg-<12 hex> (else 400)
GET · POST/api/scheduler/jobsList / create scheduled jobs. A malformed schedule (not a 5-field cron expression or an ISO-8601 datetime) or timezone (not an IANA name) is a 400
PUT/api/scheduler/jobs/{job_id}Edit a job in place (partial: only the fields sent change). Unknown id → 404
DELETE/api/scheduler/jobs/{job_id}Delete a scheduled job → {canceled: true}. Unknown id → 404 (was 200 {canceled: false})

Knowledge & skills ​

MethodPathPurpose
GET/api/knowledge/searchBrowse/search the knowledge store
POST/api/knowledge/ingestIngest a file / URL / text
POST/api/knowledge/attachAttach a chat upload (tiered inline-vs-index)
POST · PUT · DELETE/api/knowledge/chunks[/{id}]Add / edit / delete a chunk
POST/api/knowledge/delete-by-source · /api/knowledge/restore-by-sourceBulk soft-delete / restore every chunk from one ingest (reversible, grace-swept)
GET/api/playbooks · /api/playbooks/{id}List / fetch skills ("playbooks")
POST · PUT · DELETE/api/playbooks[/{id}]Create / edit / delete a skill
POST/api/playbooks/{id}/promotePromote a private skill into the commons

Memory inspector ​

The audit surface for the memory delivery layer (ADR 0069 D7): the persisted session summaries behind the <prior_sessions> digest, the hot-memory chunks (of which the newest ride each turn's injection window), and the per-turn injection record.

MethodPathPurpose
GET/api/memory/sessionsList session summaries (digest fields: id, timestamp, surface, topic, message count, size, plus in_digest: whether the session is in the current <prior_sessions> injection window)
GET · DELETE/api/memory/sessions/{session_id}Full rendered summary (what recall_session returns) / delete one
GET/api/memory/hotList hot-memory chunks (domain="hot"); each row carries injecting: whether the chunk is in the current per-turn injection window (omitted on backends without the id-attributed reader)
PUT · DELETE/api/memory/hot/{chunk_id}Edit (revision stays hot) / delete a hot chunk
GET/api/memory/injectionsPer-model-call injection records (ADR 0069 D6), newest first: which digest sessions / hot chunk ids / RAG chunk ids entered each turn, at what approximate token cost. ?session_id= filters to one session; ?limit= clamps to 1–500 (default 50)

Activity, inbox & events ​

MethodPathPurpose
GET/api/activityProvenance activity feed
GET · POST/api/inboxRead / add inbox items
POST/api/inbox/{item_id}/deliverDeliver an inbox item to the agent
GET/api/eventsServer-sent event stream (console live updates)
POST/api/events/publishPublish an event to the bus
GET · POST · PATCH · DELETE/api/tasks/...Tasks issue store (status, init, issues CRUD, close). An unknown issue id on PATCH, close or DELETE is a 404 (DELETE used to answer 200 {deleted: false}). With no task store wired, /api/tasks/status reports {initialized: false} and every other task route answers 503 "tasks not enabled"

Config, setup & settings ​

MethodPathPurpose
GET · POST/api/configRead / write langgraph-config.yaml (+ SOUL)
GET/api/config/setup-statusWizard state
POST/api/config/setup · /api/config/reset-setupComplete / reset the setup wizard
GET/api/config/presets/{name}A SOUL/archetype preset
POST/api/config/models · /api/config/test-modelList gateway models / test the connection
GET/api/settings/schemaSettings UI schema
POST/api/settings · /api/settings/resetApply / reset settings
GET/api/operationsThe ops-layer catalog — every operation (name, read/write, summary); mirrors protoagent operations (ADR 0075 D2)

Files & code pane ​

Read-only. browse is the settings folder picker and deliberately reaches outside the fs fence (names only, never contents). The other three stay inside it — the same registry.resolve chokepoint read_file uses (ADR 0007) — and file/diff never return a secret-like file's content (ADR 0112).

MethodPathPurpose
GET/api/fs/browseList the server's directories for a path picker (?path=&files=&hidden=)
GET/api/fs/roots{roots: {project: absolute root}} — the live fs fence
GET/api/fs/fileCode pane toolset only (filesystem.code_pane, default off — otherwise 404 {code: "disabled"}). ?project=&path=[&start=&end=] → {project, path, size, line_count, start, end, truncated, language, binary, text}. Lines are \n-delimited, endings preserved; capped at 2 MB / 20,000 lines / 2,000 chars per line (truncated: true, page with start=end+1). Binary → text: null. Errors carry detail: {code, reason}: bad_path 400 (unknown project / fence escape), denied 403 (secret-like name, checked before existence), not_found 404, not_a_file / bad_range (also a non-integer start/end) / unreadable 400
GET/api/fs/diffCode pane toolset only (off → 404 {code: "disabled"}). ?project= → the working tree vs HEAD: {project, is_git, head, branch, files: [{path, status: M|A|D|R|?, additions, deletions, binary, denied, old_path?, reason?, too_large?}], patch, truncated}. Hardened git (no external diff, textconv, filter, fsmonitor, hook or submodule recursion can run), scoped to the project root, untracked text files ≤ 256 KB as synthetic new-file patches, secret-like paths — and symlinks resolving outside the project or onto one — listed denied (with a reason) and content omitted, larger untracked files flagged too_large, patch capped at 1 MB and the file list at 5,000 entries (truncated: true). Not a repo → {is_git: false, files: [], patch: ""}; 10 s timeout → 504

Editor hand-off ​

Continue a console chat in Zed's agent panel. Zed can't deep-link into an agent thread, so the console (Continue in Zed) or open_in_editor offers the session and the protoagent-acp shim claims it when the operator starts a thread. In memory, per instance; one offer per project root (latest wins) and one per chat (a new offer for a chat replaces its older ones under every root; a claim removes them all), 120 s TTL, one-shot.

MethodPathPurpose
POST/api/editor/handoffBody {session_id, project?, path?, line?, title?} → {id, expires_at, root}. project resolves through the fs fence to its root; omitted → root: null, which matches any folder. Unknown session → 404 not_found; a project outside the fence → 400 unknown_project; a path escaping it → 400 bad_path; a missing session_id, a path without a project, or a line that isn't a positive integer → 400 bad_request
POST/api/editor/handoff/claimBody {cwd} → 200 {session_id, project, path, line, title} (and the offer is removed) or 204. Matches when cwd is the root, inside it, or a parent of it at most 3 levels up; the newest unexpired match wins. A filesystem/volume root (/) never matches, and the home directory itself never matches a project offer (only a project-less one)

Fleet & agents ​

MethodPathPurpose
GET · POST/api/fleetList / create workspace agents
PATCH · DELETE/api/fleet/{name}Rename / remove an agent
POST/api/fleet/{name}/{start,stop,activate} · /api/fleet/downLifecycle control
GET/api/fleet/discoverDiscover agents (LAN mDNS + tailnet)
POST · DELETE/api/fleet/remotes[/{ident}]Register / remove a remote member
POST/api/fleet/remotes/pairPair with a remote by claiming a code minted on it ({url, code, name?}); stores the per-device token, adds or re-tokens the member (ADR 0113)
GET/api/archetypesStarter agent types (catalog + installed bundles)
GET/api/archetypes/{id}/previewPeek a bundle archetype's members/MCP/secrets before install

Plugins & MCP ​

MethodPathPurpose
GET/api/plugins/installed · /api/plugins/catalog · /api/plugins/updatesInstalled / host catalog / available updates
POST/api/plugins/install · /api/plugins/syncInstall from git URL / re-sync from lock
POST/api/plugins/{id}/enabled · /api/plugins/{id}/updateEnable-disable / update one
DELETE/api/plugins/{id}Uninstall
POST/api/mcp/servers · /api/mcp/servers/importAdd / import an MCP server
DELETE/api/mcp/servers/{name}Remove an MCP server
GET/api/mcp/catalog · /api/mcp/exposedCurated server catalog / operator-MCP tools this instance exposes (effective allowlist + profile)

Telemetry & theme ​

MethodPathPurpose
GET/api/telemetry/{summary,recent,export,insights}Cost/usage telemetry
GET/api/telemetry/llm-lanesModel in-flight-limiter lane snapshot (ADR 0115)
GET · PUT · DELETE/api/themeRead / set / clear the saved theme

GET /api/telemetry/llm-lanes (operator-tier, same auth as its neighbours) returns the per-lane state of the model in-flight limiter (ADR 0115, see Operate the model in-flight limiter). It reads the per-process limiter snapshot straight from memory — no network or DB call — so it is cheap enough to poll every tick. When the limiter is off (model.max_inflight: 0, the default) it returns {"enabled": false}. When on:

json
{"enabled": true, "generated_at": "2026-09-28T17:04:11+00:00",
 "lanes": [{"lane": "https://gw/v1|protolabs/smart", "limit": 6, "reserve": 1,
            "inflight": 6, "queued": 9,
            "queued_by_priority": {"interactive": 0, "default": 2, "bulk": 7},
            "oldest_wait_s": 212.4, "wait_p50_s_5m": 38.0, "wait_p90_s_5m": 171.0,
            "queue_timeouts_5m": 2, "saturated": true}]}

One entry per lane this process has touched. saturated is true when queued > 0 has held continuously for 60 s or more; the percentiles and queue_timeouts_5m cover a rolling 5-minute window. The same snapshot is available in-process to plugins via sdk.llm_lanes().

Diagnostics ​

Member-local, read-only reads for inspecting a fleet member without shell access (#3168). Served on every member, so the hub reaches a local peer and a registered remote alike via /agents/{slug}/api/diagnostics/....

MethodPathPurpose
GET/api/diagnostics/logs?lines=NBounded tail of this member's log ring. lines is clamped (1–1000, default 200) rather than rejected; the response carries note when it was adjusted.
GET/api/diagnostics/tasks/{task_id}One exact A2A task: state, status_message, history, artifacts, accumulated_text, context_id, last_updated.

Diagnostics output is sensitive operator data — logs and task rows can carry prompts, user content, and tool arguments. Both endpoints are operator-tier (the ADR 0066 federation credential is denied /api outright), read-only, bounded in history/artifact/text size, and scrubbed by the shared credential redactor before returning.

Responses degrade rather than 500: an unknown task is 404, a member with no task store is 503, and a malformed store row returns 200 with the unparseable columns named in malformed[]. A stopped or unreachable member is the proxy's case and answers 409/502/504 from /agents/{slug}/*. Truncation is always reported in truncated[] — a partial history is never presented as a complete one.

The log source is an in-process ring buffer sized by LOG_BUFFER_LINES, not agent.log; see environment variables for why.

Part of the protoLabs autonomous development studio.