CLI Agent Orchestrator API Documentation
July 8, 2026 · View on GitHub
Base URL: http://localhost:9889 (default)
Health Check
GET /health
Check if the server is running.
Response:
{
"status": "ok",
"service": "cli-agent-orchestrator"
}
Providers
GET /agents/providers
List available providers with installation status.
Response: Array of provider objects
[
{
"name": "kiro_cli",
"binary": "kiro-cli",
"installed": true
},
{
"name": "claude_code",
"binary": "claude",
"installed": true
},
{
"name": "codex",
"binary": "codex",
"installed": true
},
{
"name": "kimi_cli",
"binary": "kimi",
"installed": false
},
{
"name": "hermes",
"binary": "hermes",
"installed": true
},
{
"name": "copilot_cli",
"binary": "copilot",
"installed": false
}
]
Note: The installed field checks if the provider binary is available in the system PATH via shutil.which().
Sessions
POST /sessions
Create a new session with one terminal.
Parameters:
provider(string, required): Provider type ("kiro_cli", "claude_code", "codex", "antigravity_cli", "hermes", "kimi_cli", "copilot_cli", "opencode_cli", or "cursor_cli")agent_profile(string, required): Agent profile namesession_name(string, optional): Custom session nameworking_directory(string, optional): Working directory for the agent session
Response: Terminal object (201 Created)
GET /sessions
List all sessions.
Response: Array of session objects
GET /sessions/{session_name}
Get details of a specific session.
Response: Session object with terminals list
DELETE /sessions/{session_name}
Delete a session and all its terminals.
Response:
{
"success": true
}
Terminals
Note: All terminal_id path parameters must be 8-character hexadecimal strings (e.g., "a1b2c3d4").
POST /sessions/{session_name}/terminals
Create an additional terminal in an existing session.
Parameters:
provider(string, required): Provider typeagent_profile(string, required): Agent profile nameworking_directory(string, optional): Working directory for the terminalallowed_tools(string, optional): Comma-separated list of allowed CAO tools for the worker.caller_id(string, optional): Terminal ID of the creating terminal (8-character hexadecimal). Recorded sosend_messagecan default replies to the caller (issue #284).defer_init(bool, optional, defaultfalse): Whentrue, return as soon as the tmux window and DB record exist, without waiting forprovider.initialize()to finish. The provider is still created and initialized — but on a background asyncio task on cao-server, so the HTTP round-trip stays under ~2s regardless of provider startup latency. Used by the MCPassigntool to keep tool-call latency well under kiro-cli 2.11's ~60s per-tool client timeout, and to allow multiple concurrent assigns to run their init phases in parallel.
Request body (optional, JSON): the deferred-init message payload is sent in the body — not query params — so prompt content is not exposed in HTTP access logs and is not subject to URL-length limits.
initial_message(string, optional): Whendefer_init=true, this message is delivered to the newly created worker viasend_inputafterprovider.initialize()completes. Ignored ifdefer_init=false. Ordering: init runs first, then message delivery, both on the same background task.initial_message_orchestration_type(string, optional): One ofassignorhandoff. Passed through tosend_inputfor plugin event emission wheninitial_messageis delivered.
Response: Terminal object (201 Created). When defer_init=true, the returned status is unknown (the provider is still initializing on a background task); poll GET /terminals/{id} for the live status before sending further input.
GET /sessions/{session_name}/terminals
List all terminals in a session.
Response: Array of terminal objects
GET /terminals/{terminal_id}
Get terminal details.
Response: Terminal object
{
"id": "string",
"name": "string",
"provider": "kiro_cli|claude_code|codex|antigravity_cli|hermes|kimi_cli|copilot_cli|opencode_cli|cursor_cli",
"session_name": "string",
"agent_profile": "string",
"caller_id": "string|null",
"status": "idle|processing|completed|waiting_user_answer|error",
"last_active": "timestamp"
}
POST /terminals/{terminal_id}/input
Send input to a terminal.
Parameters:
message(string, required): Message to send
Response:
{
"success": true
}
POST /terminals/{terminal_id}/key
Send a tmux key sequence to a terminal. Use this for interactive prompts that require non-text key presses, such as Hermes clarify picker navigation.
The endpoint is generic, but the only in-tree structured consumer today is the
Hermes path of answer_user_prompt. Other providers can use it in the future
when they expose equivalent prompt states or key-navigation flows.
Parameters:
key(string, required): allowed tmux key name:Up,Down,Left,Right,Enter,Tab,Escape,Space, a single alphanumeric key, or aC-,M-, orS-modifier combo such asC-corM-x
Response:
{
"success": true
}
GET /terminals/{terminal_id}/output
Get terminal output.
Parameters:
mode(string, optional): Output mode - "full" (default), "last", or "tail""full"returns the StatusMonitor rolling buffer (most recent ~8KB of streamed output), not unbounded scrollback. Long sessions are truncated to the tail; use the on-disk terminal log for complete history.
Response:
{
"output": "string",
"mode": "string"
}
GET /terminals/{terminal_id}/working-directory
Get the current working directory of a terminal's pane.
Response:
{
"working_directory": "/home/user/project"
}
Note: Returns null if working directory is unavailable.
POST /terminals/{terminal_id}/exit
Send provider-specific exit command to terminal.
Behavior:
- Calls the provider's
exit_cli()method to get the exit command - Text commands (e.g.,
/exit,quit) are sent as literal text viasend_input() - Key sequences prefixed with
C-orM-(e.g.,C-dfor Ctrl+D) are sent as tmux key sequences viasend_special_key(), which tmux interprets as actual key presses
| Provider | Exit Command | Type |
|---|---|---|
| kiro_cli | /exit | Text |
| claude_code | /exit | Text |
| codex | /exit | Text |
| antigravity_cli | /exit | Text |
| hermes | /exit | Text |
| kimi_cli | /exit | Text |
| copilot_cli | /exit | Text |
Response:
{
"success": true
}
DELETE /terminals/{terminal_id}
Delete a terminal.
Response:
{
"success": true
}
Inbox (Terminal-to-Terminal Messaging)
POST /terminals/{receiver_id}/inbox/messages
Send a message to another terminal's inbox.
Parameters:
sender_id(string, required): Sender terminal IDmessage(string, required): Message content
Response:
{
"success": true,
"message_id": "string",
"sender_id": "string",
"receiver_id": "string",
"created_at": "timestamp"
}
Behavior:
- Messages are queued and delivered when the receiver terminal is IDLE
- Messages are delivered in order (oldest first)
- Delivery is automatic via event-driven status detection
Memory
REST mirror of the cao memory CLI. All /memory endpoints return 404 with
"Memory system is disabled" when memory.enabled is false in settings.json;
use GET /settings/memory to discover the enabled state (e.g. for hiding UI).
Keys must match ^[a-z0-9-]{1,60}$ and scope_id must match
^[a-zA-Z0-9._-]{1,128}$; malformed values return 422.
Because the server's working directory is not the user's project, project scope
is addressed by an explicit scope_id query parameter (the resolved project
ID). This intentionally diverges from the MCP memory_forget tool, which
resolves context from the calling terminal.
Known inconsistency: the internal GET /terminals/{id}/memory-context endpoint
predates this contract and returns an empty 200 (not 404) when memory is
disabled.
GET /settings/memory
Return whether the memory subsystem is enabled.
Response:
{
"enabled": true
}
GET /memory
List stored memories across all projects (the CLI's cao memory list --all).
Parameters:
scope(string, optional): Filter by scope (global,project,session,agent)type(string, optional): Filter by memory type (user,feedback,project,reference)scope_id(string, optional): Filter to one project/session/agentlimit(integer, optional): Max results, 1–100 (default: 50)
Response:
[
{
"key": "string",
"scope": "string",
"scope_id": "string|null",
"memory_type": "string",
"tags": "string",
"created_at": "timestamp",
"updated_at": "timestamp"
}
]
scope_id is the project ID for project memories, the session/agent ID for
those scopes, and null for global.
GET /memory/export
Export one memory scope as an archive bundle (the CLI's cao memory export).
Streams a gzipped tarball of the OKF bundle (topic files plus index.md and
manifest.md).
Parameters:
scope(string, required): Scope to export (global,project, orfederated;400for the privatesession/agentscopes — there is no include-private escape hatch over HTTP)format(string, optional): Archive format (default:okf;400on unknown formats)scope_id(string): Required forprojectscope (400if missing)include_history(boolean, optional): Includehistory/<key>.mdfiles (default:false)redact(boolean, optional): Redact secret matches instead of skipping the topic (default:false)
Response: 200 with Content-Type: application/gzip — the bundle tarball
as the response body.
When API auth is enabled, this endpoint requires a token carrying at least the
read scope (cao:read, cao:write, or cao:admin); requests without one are
403'd.
GET /memory/{key}
Show a memory by key (first match wins when the same key exists in several
scopes; narrow with scope/scope_id).
Parameters:
scope(string, optional): Scope to search inscope_id(string, optional): Project/session/agent to search in
Response: the list entry shape plus "content" (the latest wiki section).
404 if no exact key match.
DELETE /memory/{key}
Delete a memory by key.
Parameters:
scope(string, optional): Scope of the memory (default:project)scope_id(string): Required forproject,session, andagentscopes (400if missing)
Response:
{
"success": true
}
404 if the key does not exist in the scope.
DELETE /memory
Clear all memories in a scope. Best-effort: deletion continues past per-item failures and reports how many were removed.
Parameters:
scope(string, required): Scope to clearscope_id(string): Required forproject,session, andagentscopes (400if missing)
Response:
{
"success": true,
"deleted_count": 3
}
Error Responses
All endpoints return standard HTTP status codes:
200 OK: Success201 Created: Resource created400 Bad Request: Invalid parameters404 Not Found: Resource not found500 Internal Server Error: Server error
Error response format:
{
"detail": "Error message"
}