51 KiB
Omnigent Server API
Four namespaces: agent management (/api/agents), conversations
(/v1/conversations), sessions (/v1/sessions), and session
resources (/v1/sessions/{session_id}/resources).
Compatibility Reference
| Namespace | Compatible with | Reference implementation |
|---|---|---|
Agent Management (/api/agents) |
Omnigent (ours) | No external reference — this is our own API. |
Conversations (/v1/conversations) |
Omnigent (ours) | No external reference. |
Sessions (/v1/sessions) |
Omnigent (ours) | No external reference. Session-first API for long-running agent interactions. See "Sessions API" section below. |
Session file resources (/v1/sessions/{session_id}/resources/files) |
Omnigent (ours) | No external reference. Files are scoped to their owning session. |
Agent Management
Create Agent
POST /api/agents
Content-Type: multipart/form-data
Parts:
bundle: <tarball> required — must contain config.yaml with a unique
name and optional description. The name becomes
the "model" for inference requests.
The server validates the bundle on upload: extracts it to a temporary
directory, parses config.yaml, and runs the spec validator. Name and
description are derived from the spec — no separate form fields.
201 Created
{
"id": "ag_abc123",
"object": "agent",
"name": "my-agent",
"description": "...",
"created_at": 1774118382
}
409 Conflict — name already exists
400 Bad Request — invalid bundle (corrupt tarball, missing config.yaml,
spec validation failure, missing name, path traversal, etc.)
List Agents
GET /api/agents
Query parameters:
limit (integer, optional, default: 20, max: 1000)
Number of agents to return.
after (string, optional)
Cursor for forward pagination. Pass the `last_id` from a previous response
to get the next page.
before (string, optional)
Cursor for backward pagination. Pass the `first_id` from a previous response
to get the previous page.
order (string, optional, default: "desc")
Sort order by `created_at`. Either "asc" or "desc".
200 OK
{
"object": "list",
"data": [
{"id": "ag_abc123", "object": "agent", "name": "my-agent", ...},
{"id": "ag_def456", "object": "agent", "name": "other-agent", ...}
],
"first_id": "ag_abc123",
"last_id": "ag_def456",
"has_more": false
}
Items in data have the same shape as the create/get response.
Get Agent
GET /api/agents/{id}
200 OK — same shape as create response
404 Not Found
Delete Agent
DELETE /api/agents/{id}
200 OK
{"id": "ag_abc123", "object": "agent.deleted", "deleted": true}
404 Not Found
Cancels all in-flight responses for this agent before deleting.
Session File Resources
Upload files that can be referenced by file_id in input_image and input_file
content types. Files are immutable once uploaded and are scoped to their owning
session. The legacy global /v1/files endpoints are removed; clients must use
the session resource namespace.
Upload File
POST /v1/sessions/{session_id}/resources/files
Content-Type: multipart/form-data
Parts:
file: <binary> required
201 Created
{
"id": "file_abc123",
"object": "session.resource",
"type": "file",
"session_id": "conv_abc123",
"name": "report.pdf",
"metadata": {
"filename": "report.pdf",
"bytes": 214961,
"created_at": 1774118382
}
}
400 Bad Request — missing file
404 Not Found — session not found
List Files
GET /v1/sessions/{session_id}/resources/files
Query parameters:
limit (integer, optional, default: 20, max: 1000)
after (string, optional)
before (string, optional)
order (string, optional, default: "desc")
Sort order by `created_at`. Either "asc" or "desc".
200 OK
{
"object": "list",
"data": [
{"id": "file_abc123", "object": "session.resource", "type": "file", ...},
{"id": "file_def456", "object": "session.resource", "type": "file", ...}
],
"first_id": "file_abc123",
"last_id": "file_def456",
"has_more": false
}
Get File
GET /v1/sessions/{session_id}/resources/files/{id}
200 OK — same shape as upload response
404 Not Found — session or file not found, including files owned by another session
Delete File
DELETE /v1/sessions/{session_id}/resources/files/{id}
200 OK
{"id": "file_abc123", "object": "session.resource.deleted", "deleted": true}
404 Not Found
Get File Content
GET /v1/sessions/{session_id}/resources/files/{id}/content
200 OK
Content-Type: <original media type>
<binary content>
404 Not Found
Conversations
Conversations are created automatically. When a response has no previous_response_id,
the server creates a new conversation and assigns the response to it. When a response
has a previous_response_id pointing to the latest response in a conversation,
it joins that conversation.
When previous_response_id points to a non-latest response (a fork), the server
creates a new conversation. Items up to and including the fork point are copied into
the new conversation with new response IDs, and the new response is added there.
The original conversation is unchanged. Each conversation is always a linear thread
— no branching. Response IDs are globally unique, so previous_response_id is
never ambiguous across conversations.
Clients may optionally pass a conversation ID when creating responses (must be
paired with previous_response_id). Conversation APIs are primarily for
retrieval — listing past conversations, loading message history, and finding
the latest response ID to continue from.
List Conversations
GET /v1/conversations
Query parameters:
limit (integer, optional, default: 20, max: 1000)
Number of conversations to return.
after (string, optional)
Cursor for forward pagination. Pass the `last_id` from a previous response.
before (string, optional)
Cursor for backward pagination. Pass the `first_id` from a previous response.
order (string, optional, default: "desc")
Sort order. Either "asc" or "desc".
sort_by (string, optional, default: "created_at")
Column to sort on. Either "created_at" or "updated_at".
200 OK
{
"object": "list",
"data": [
{"id": "conv_abc123", "object": "conversation", "title": null, "created_at": ..., "updated_at": ...},
{"id": "conv_def456", "object": "conversation", "title": "Weather chat", "created_at": ..., "updated_at": ...}
],
"first_id": "conv_abc123",
"last_id": "conv_def456",
"has_more": false
}
Results ordered by sort_by column descending (newest first) by default.
Get Conversation
GET /v1/conversations/{id}
200 OK
{
"id": "conv_abc123",
"object": "conversation",
"title": null,
"created_at": 1774118382,
"updated_at": 1774118400
}
404 Not Found
List Conversation Items
GET /v1/conversations/{id}/items
Query parameters:
limit (integer, optional, default: 20, max: 1000)
after (string, optional)
before (string, optional)
order (string, optional, default: "asc")
Sort order by position in conversation. Either "asc" (chronological) or "desc".
200 OK
{
"object": "list",
"data": [
{"id": "msg_aaa", "response_id": "resp_001", "type": "message",
"role": "user", "status": "completed",
"content": [{"type": "input_text", "text": "What's the weather?"}]},
{"id": "msg_bbb", "response_id": "resp_001", "model": "my-agent", "type": "message",
"role": "assistant", "status": "completed",
"content": [{"type": "output_text", "text": "It's sunny in SF.", "annotations": []}]},
{"id": "msg_ccc", "response_id": "resp_002", "type": "message",
"role": "user", "status": "completed",
"content": [{"type": "input_text", "text": "And tomorrow?"}]},
{"id": "fc_ddd", "response_id": "resp_002", "model": "my-agent", "type": "function_call",
"status": "completed", "name": "get_weather",
"arguments": "{\"location\": \"SF\", \"date\": \"tomorrow\"}", "call_id": "call_001"},
{"id": "fco_eee", "response_id": "resp_002", "type": "function_call_output",
"status": "completed",
"call_id": "call_001", "output": "{\"forecast\": \"rain\", \"high\": 58}"},
{"id": "msg_fff", "response_id": "resp_002", "model": "my-agent", "type": "message",
"role": "assistant", "status": "completed",
"content": [{"type": "output_text", "text": "Rain expected, high of 58°F.", "annotations": []}]}
],
"first_id": "msg_aaa",
"last_id": "msg_fff",
"has_more": false
}
404 Not Found — conversation doesn't exist
Items include all input and output messages, function calls, and function call
outputs accumulated across all responses in this conversation. Each item carries
a response_id linking it to the response that produced it. Model-produced items
(assistant messages, function calls, reasoning) include a model field identifying
the agent. User messages and function call outputs do not have model — the agent
is always recoverable from response_id if needed. To continue a conversation,
pass the response_id from the last item as previous_response_id.
Update Conversation
PATCH /v1/conversations/{id}
Content-Type: application/json
{"title": "Weather chat"}
200 OK
{
"id": "conv_abc123",
"object": "conversation",
"title": "Weather chat",
"created_at": 1774118382,
"updated_at": 1774118400
}
404 Not Found
400 Bad Request — invalid field
Currently only title (string | null) is updatable.
Delete Conversation
DELETE /v1/conversations/{id}
200 OK
{"id": "conv_abc123", "object": "conversation.deleted", "deleted": true}
404 Not Found
Deletes the conversation and all associated tasks. Cancels any in-flight tasks in the conversation before deleting.
Sessions API (/v1/sessions)
A session-first API for long-running agent interactions. The session is the primary resource. Tasks (responses) are internal implementation details.
A session is a thin layer on top of a conversation: same backing store, same items, same live SSE publisher. The session API exposes four endpoints (create, get, post-event, stream) and a small set of session-scoped events on the wire (see "Stream Events" below).
Compatibility Reference
| Namespace | Compatible with | Notes |
|---|---|---|
Sessions (/v1/sessions) |
Omnigent (ours) | No external reference. Purpose-built for agent-native workflows: live tail, queued input, interrupt. |
Session Lifecycle States
A session is always in one of these states:
idle -> running -> idle
-> waiting -> running -> idle
-> failed
- idle: No agent loop running. The session is ready to accept
new events. This is the initial state when no
initial_itemsare posted on creation, and the terminal state after a turn finishes. - running: An agent loop is actively processing. The session's SSE stream is emitting events. New events can still be posted — they queue behind the current turn and are consumed at the next iteration checkpoint.
- waiting: The agent loop has parked the current turn waiting
for an external signal (background tools, sub-agent completion,
client tool result). Surfaced as a
session.statusevent withstatus: "waiting". The loop resumes (back torunning) when the signal arrives. - failed: An unrecoverable error occurred during processing. The session cannot accept new events.
action_required is intentionally NOT a session status — it lives at
the task layer (see runtime). The session schema's status field is a
Literal["idle", "running", "waiting", "failed"] and the route layer
rejects any other value with a 500 (fail loud).
Session Object
The SessionResponse Pydantic model (defined in server/schemas.py)
is returned by JSON POST /v1/sessions, GET /v1/sessions/{id}, and
PATCH /v1/sessions/{id}:
{
"id": "conv_abc123",
"agent_id": "ag_abc123",
"status": "running",
"created_at": 1234567890,
"runner_id": "runner_abc123",
"items": [
{"id": "msg_aaa", "type": "message", "role": "user", "status": "completed",
"content": [{"type": "input_text", "text": "Plan my trip"}]}
]
}
Fields:
id (string, required)
Unique session identifier; also the underlying conversation ID.
Prefixed with conv_.
agent_id (string, required)
Durable identifier of the bound agent (e.g. "ag_abc123"). Stable
across renames of the agent. The session is bound by ID, not name -
name lookups are not supported.
agent_name (string, optional)
Human-readable display name of the bound agent (e.g.
"claude-native-ui"). For switch-created session-scoped clones
this is the spec's clean name, not the clone row's
"… (switch ag_…)" disambiguation name. Changes when the session
is switched to a different agent in place
(POST /v1/sessions/{id}/switch-agent), so attached clients can
refresh their displayed agent label. null when the server cannot
resolve the agent row.
status (string, required)
Current lifecycle state: "idle", "running", "waiting", or
"failed".
created_at (integer, required) Unix timestamp (seconds) when the session was created.
runner_id (string or null)
Mutable runner binding for this session. null until the client
binds a registered runner with PATCH /v1/sessions/{id}.
external_session_id (string or null)
Runtime-native session id this conversation wraps (e.g. Claude
Code's session uuid for omnigent claude sessions). Populated
by the wrapper bridge from the underlying runtime. null for
regular AP-only conversations. Generic across runtimes — at most
one external session per conversation.
model_override (string or null)
Per-session LLM model override, e.g. "claude-opus-4-7". null
means no override is active and the bound agent's spec model
applies. Persisted on conversations.model_override; set via
PATCH /v1/sessions/{id} (also the path the REPL's /model
command uses) so the web picker and the TUI stay in sync.
cost_control_mode_override (string or null)
Per-session cost-control switch: "on" activates the spec's
configured cost-control mode, "off" disables cost control for
this session, null defers to the spec default. Persisted on
conversations.cost_control_mode_override; set at create or via
PATCH /v1/sessions/{id} (the web "Cost Optimized" toggle) and
read by the cost-control advisor pipeline at turn start.
git_branch (string or null)
Git branch checked out in the session's worktree, e.g.
"feature/login". Set only when the session was created with a
server-created git worktree (the git block of create); null
otherwise. A non-null value drives the "delete local branch"
cleanup option on session delete. See
designs/SESSION_GIT_WORKTREE.md.
items (array, default [])
Committed conversation items in chronological order: user
messages, assistant messages, function calls, function-call
outputs, reasoning. Each item shape matches the
GET /v1/conversations/{id}/items schema.
pending_elicitations (array, default [])
Outstanding response.elicitation_request event payloads at
snapshot build time. Replayed by the client as ApprovalCard blocks
on cold load, since the live SSE stream has no replay buffer.
pending_inputs (array, default [])
Un-consumed web-composer user messages on native-terminal
(claude-native / codex-native) sessions at snapshot build time,
each {pending_id, content}. Native sessions don't persist a web
message at POST time (the transcript forwarder is the single
writer), so the Omnigent server holds these in-memory and replays them
here — the client re-hydrates the optimistic "queued message"
bubble so it survives navigation / an SSE rebind. Drained when the
message round-trips back (the matching session.input.consumed
carries cleared_pending_id). Empty for non-native sessions, which
already carry the message in items.
todos (array, default [])
Current Claude Code todo list for omnigent claude sessions.
Each item: {content: string, status: "pending"|"in_progress"|"completed", activeForm: string} where activeForm is the gerund form of the
current activity (e.g. "Running tests"). Sourced from the
server's in-memory todo cache (updated by external_session_todos
events). Empty for non-claude-native sessions or before the first
turn creates todos.
terminal_pending (boolean, default false)
true while the runner is auto-creating the terminal for a
terminal-first session (claude-native / codex-native), so the Web UI
shows a spinner on the Terminal pill instead of a silent greyed-out
button. Cleared to false once the terminal lands or auto-create
fails; from then on the client relies purely on whether a terminal
resource exists. Set by two paths: (1) directly by the Omnigent server at
session creation for host-launched terminal-first sessions, so the
spinner appears immediately before the runner even starts; (2) by the
relay when the runner's session.terminal_pending events arrive,
covering non-host-launched sessions. The in-memory cache is read at
snapshot time so a client connecting mid-spin-up still sees the spinner.
Create Session
Two request shapes are accepted.
Create From Uploaded Agent Bundle
POST /v1/sessions
Content-Type: multipart/form-data
metadata: JSON string part, e.g. {"title": "debug auth flow"}
bundle: agent .tar.gz file part
Request parts:
metadata (JSON string, required)
Session metadata. Shape matches SessionCreateMetadata:
{title?: string | null, labels?: object, reasoning_effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | null, workspace?: string | null, terminal_launch_args?: string[] | null}.
terminal_launch_args carries pass-through CLI args for a native
terminal wrapper, e.g. ["--permission-mode", "bypassPermissions"]
(same field as the JSON create path below). Unknown fields fail with 400.
bundle (file, required)
Agent tarball. The server validates it exactly like
POST /api/agents: extract the bundle, load config.yaml, and
require a spec name.
201 Created:
{"session_id": "conv_abc123"}
The server stores the bundle, then creates the conversations row
and the session-scoped agents row in one database transaction. The
new agent row has agents.kind set to 'session',
and conversations.agent_id points at that agent. If the database
agent write fails, the conversation row rolls back. If multipart or
bundle parsing fails, no database row is written.
400 Bad Request - invalid metadata or invalid agent bundle 409 Conflict - agent write violates a uniqueness constraint 422 Unprocessable Entity - required multipart part is missing
Create From Existing Agent
POST /v1/sessions
Content-Type: application/json
{
"agent_id": "ag_abc123",
"initial_items": [],
"title": "debug auth flow",
"labels": {"env": "test"},
"host_id": "host_abc123",
"workspace": "/Users/alice/myrepo",
"git": {"branch_name": "feature/login", "base_branch": "main"},
"terminal_launch_args": ["--permission-mode", "bypassPermissions"]
}
This preserves the existing sessions API contract for clients that
already uploaded or registered an agent. The response is the full
SessionResponse shape with id set to the new conversation id.
host_type (string, "external" | "managed", default "external")
How the session's host is obtained. "external" (the default,
and the pre-existing behavior): the session runs on
caller-managed compute — a host registered via omnigent host
(pass host_id) or a caller-managed runner (no host_id).
"managed": the SERVER provisions a sandbox host from its
sandbox: config (see omnigent/server/managed_hosts.py),
starts omnigent host inside it, binds the session to it, and
launches the runner there. With "managed", host_id and
workspace must NOT be set (422) — the server chooses both.
Provisioning runs in the BACKGROUND: the create returns
immediately with host_id / workspace null, and they appear
on GET /v1/sessions/{id} once the sandbox host registers. A
message POSTed before the launch settles waits for it (and
reports the launch failure as 503 if it failed) instead of
failing with "no runner bound". 400 when the server has no
sandbox: config or the configured provider lacks managed
support. Deleting a managed session terminates its sandbox
(best-effort), including a session deleted mid-provision.
host_id (string or null)
Host to launch the runner on. When set, workspace is required
and validated against the agent's os_env.cwd boundary on that
host (see designs/SESSION_WORKSPACE_SELECTION.md).
workspace (string or null)
Absolute path on the host. Required when host_id is set. When
git is also set, this is the source repository directory; the
created worktree becomes the stored workspace.
git (object or null)
Optional git worktree options. When present (requires host_id),
the server creates a worktree for a new branch on the host and
starts the runner in it. Shape: {branch_name: string, base_branch?: string | null}. branch_name is validated against
git ref-format rules. See designs/SESSION_GIT_WORKTREE.md.
terminal_launch_args (array of strings or null)
Optional pass-through CLI args for a native terminal wrapper
(claude / codex), e.g. ["--permission-mode", "bypassPermissions"]
(the web UI's permission-mode selector). Set at create time so the
runner has them on the session row before it auto-launches the
terminal. The flat-list shape is the security
boundary — no key for a caller to smuggle launch wiring (bridge
dir, Omnigent URL, auth), which stay runner-owned. Bounds (count /
length) are validated server-side; a malformed list returns 400.
null for non-native sessions. Settable later via
PATCH /v1/sessions/{id} (last-write-wins). See
designs/NATIVE_RUNNER_SERVER_LAUNCH.md.
400 Bad Request - invalid terminal_launch_args (count / length bounds) 404 Not Found - no agent with that id 422 Unprocessable Entity - invalid request body
List Sessions
GET /v1/sessions
200 OK — paginated list whose `data` entries match `SessionResponse`
minus `items` and snapshot-only fields.
Supports cursor pagination and filters such as search_query and
include_archived.
The kind filter scopes which conversation kinds are listed:
default (the default) returns only top-level user-initiated
sessions, sub_agent returns only sub-agent child sessions, and
any returns both. any powers the new-session agent picker's
discovery of agents that are only bound to sub-agent sessions.
When liveness is wired, each list item includes two orthogonal signals
(matching GET /health for the same session id and the
WS /v1/sessions/updates list item fields):
runner_online (boolean)
Strict runner liveness — true iff a runner tunnel is currently
registered. The sole reachability signal (a dead runner on a live
host reads false here, not true).
host_online (boolean | null)
Whether the session's host tunnel is live (status online and fresh
within the host liveness TTL). null when the session has no host
binding. Used to distinguish "runner asleep, host alive — send a
message to relaunch" from "host offline — reconnect/fork"; never
folded into reachability.
Get Session (Snapshot)
GET /v1/sessions/{session_id}[?include_items=true&include_liveness=true&refresh_state=false]
200 OK — body matches the `SessionResponse` shape above.
404 Not Found — no session with that id
Returns the current snapshot: identity, lifecycle status, all committed items, and any queued (unconsumed) inputs. Combined with the live stream, this is the reconnect contract — see "Reconnect Contract" below.
include_items (query param, boolean, default true)
When false, the committed-items read is skipped and items is
[] (an empty list, not an absent field). For callers that
hydrate the transcript through the paginated
GET /sessions/{id}/items endpoint (the web chat surface), the
snapshot's copy is redundant and the items read is the most
expensive step of the snapshot build.
include_liveness (query param, boolean, default true)
When false, the runner/host liveness lookup is skipped and
runner_online/host_online stay unset. For callers that source
liveness from the /health poll and the live stream (the web
chat surface), the snapshot's copy is redundant.
refresh_state (query param, boolean, default false)
When true, runner-derived snapshot overlays (for example skills
and Codex-native model options) are refreshed from the bound runner
instead of served from AP-process memory. Browser reload/bind
requests use this so a page refresh pierces stale capability
caches after a server-side bug fix.
When runner liveness is wired (and not skipped via
include_liveness=false), the snapshot includes:
runner_online (boolean)
Whether the session's bound runner/host is reachable. This is
session-scoped (authorized by access to the session), and matches
GET /health?session_id=... for the same id.
Delete Session
DELETE /v1/sessions/{session_id}[?delete_branch=true]
200 OK — {"id": "conv_abc123", "deleted": true}
404 Not Found — no session with that id
403 Forbidden — caller is not the session owner
Requires owner-level access. Tears down runner-side resources, session files, and the conversation row.
delete_branch (query param, boolean, default false)
Opt-in git cleanup. When true and the session has a
server-created worktree (git_branch set), the host removes the
worktree directory and deletes its branch (git worktree remove --force then git branch -D). Ignored for sessions with no
worktree. Best-effort: a cleanup failure does not block the
delete. See designs/SESSION_GIT_WORKTREE.md.
Bind Session Runner
PATCH /v1/sessions/{session_id}
Content-Type: application/json
{
"runner_id": "runner_abc123",
"title": "debug auth flow",
"labels": {"env": "test"},
"reasoning_effort": "high",
"model_override": "claude-opus-4-7",
"collaboration_mode": "plan",
"external_session_id": "a1b2c3d4-1234-5678-9abc-def012345678"
}
Request body:
runner_id (string, optional) Runner id that is already registered with the server's tunnel registry. Empty strings fail with 400.
title (string or null, optional)
Session title update. null leaves the title unchanged.
labels (object, optional) Labels to merge into the existing session labels.
reasoning_effort (string or null, optional)
Per-session reasoning-effort hint. Accepted metadata values are
none, minimal, low, medium, high, xhigh, and max.
Provider-specific support is validated when a turn executes.
Clear values follow the existing sessions API semantics.
model_override (string or null, optional)
Per-session LLM model identifier the workflow should use on
subsequent turns instead of the agent spec's llm.model,
e.g. "claude-opus-4-7". The server does not enumerate valid
models — the executor validates at turn start. Clear aliases
"default", "off", and "reset" remove the override
(matching the REPL /model command); empty / whitespace-only
strings fail with 400 rather than silently clearing.
Leaves the column unchanged when omitted or null.
collaboration_mode (string, optional)
Codex-native collaboration-mode string. "plan" enters Plan mode;
"default" returns to Default mode. Only valid for sessions whose
wrapper label is codex-native-ui. Explicit toggles are forwarded
to the live runner as plan_mode_change before the label is
persisted; if no live runner or loaded Codex bridge can apply the
change, the request fails and the stored mode is left unchanged.
cost_control_mode_override (string or null, optional)
Per-session cost-control switch: "on" activates the spec's
configured cost-control mode, "off" disables cost control for
this session. Explicit null clears the override back to the
spec default; omitting the field leaves the stored value
unchanged ("off" is a real value here, so field presence — not
a clear alias — is the clear signal). Any other value fails with
400 invalid_input.
external_session_id (string, optional)
Runtime-native session id this conversation wraps (e.g. Claude
Code's session uuid). Idempotent on same-value writes; the server
rejects attempts to overwrite an already-set different value with
400 invalid_input. Wrapper bridges should write the value once
when they first observe it from the underlying runtime.
200 OK - body matches the SessionResponse shape above, with
runner_id set to the newly bound value when runner_id was present.
400 Bad Request - runner is not currently registered; collaboration_mode
is used on a non-Codex-native session; or external_session_id would
overwrite a different existing value
404 Not Found - no session with that id
This is the mutable affinity primitive for Alpha. The same endpoint
serves create-bind, resume-bind, and recover-bind: the client starts a
runner, waits for registration, then PATCHes the session to the new
runner id. The write replaces any previous value in
conversations.runner_id; no history table is maintained.
Codex-specific APIs
Codex-native session routes, including the Codex Goal subresource, are documented in codex-API.md.
Post Event
POST /v1/sessions/{session_id}/events
Content-Type: application/json
{
"type": "message",
"data": {
"role": "user",
"content": [{"type": "input_text", "text": "What about hotels?"}]
}
}
Request body matches `SessionEventInput`:
type (string, required)
Event/input discriminator. Recognized values:
- "message" — a user message
- "function_call_output" — a client-side tool result
- "function_call" — a queued tool call (rare; mostly
emitted by the runtime, not clients)
- "reasoning" — a queued reasoning item
- "tool_result" — alias surfaced by some clients;
see `ITEM_TYPE_TO_DATA_CLS` for the
canonical set
- "interrupt" — preempt the running loop (see below)
- "compact" — explicit context compaction. Forwarded
to the bound runner; for claude-native
sessions the runner injects `/compact`
into the terminal (Claude Code compacts
its own context) and the Omnigent server
skips its own compaction. For in-process
harnesses the runner 204 no-ops and the
Omnigent server runs the compaction itself
(summarises history, persists a
`compaction` item). Payload: `{}`.
- "stop_session" — terminate the live session without
deleting the conversation (owner-only;
requires LEVEL_OWNER). Forwarded
harness-agnostically to the bound
runner, which hard-kills the external
process for harnesses that have one
(claude-native kills its tmux pane) and
204s for in-process harnesses; for a
host-launched session the host's runner
subprocess is stopped too, so its tunnel
drops and `runner_online` flips false.
Stop is non-sticky: no persistent marker
is written, so the next message
auto-relaunches a runner on a live host
(there is no separate "resume" event).
The conversation transcript is
preserved. Returns `{queued: false}`.
- "external_conversation_item"
— internal terminal-observed item
envelope; appends/broadcasts without
starting a duplicate task
- "external_output_text_delta"
— internal terminal-observed assistant
text delta; publishes a transient
`response.output_text.delta` SSE event
without persisting an item or starting
a task. Payload: `{delta: string}`.
The final completed message still
arrives through
`external_conversation_item`.
- "external_session_status"
— internal terminal-observed status edge;
publishes a `session.status` event with
data `{status: "running" | "waiting" |
"idle" | "failed"}`
- "external_session_usage"
— internal terminal-observed token-usage
update; persists `context_tokens` /
`context_window` on conversation labels
and publishes a `session.usage` event.
Payload: at least one of
`{context_tokens: int, context_window: int}`
(native harnesses may also send cumulative
`cumulative_cost_usd` / `cumulative_input_tokens`
/ `cumulative_output_tokens` / `model`). The
published `session.usage` event additionally
carries `total_cost_usd` (cumulative session
spend, USD) when the session is priced; it is
omitted when unpriced. The same value is seeded
on the session snapshot as `total_cost_usd`
(`null` when unpriced).
- "external_reasoning_effort_change"
— internal terminal-observed thinking-level
update from a native forwarder. Persists
`reasoning_effort` and publishes a
`session.reasoning_effort` event. Payload:
`{reasoning_effort: string | null}`; `null`
clears to the model default.
- "external_codex_collaboration_mode_change"
— internal Codex app-server collaboration-mode
update. Persists the mode kind as label
`omnigent.codex_native.collaboration_mode`.
Payload: `{mode: "default" | "plan"}`.
- "external_compaction_status"
— internal terminal-observed compaction
edge from the claude-native forwarder
(Claude Code's `PreCompact` and
post-compaction `SessionStart
source=compact` hooks). Republishes as
`response.compaction.in_progress` /
`.completed` / `.failed` so the web UI
brackets Claude's own terminal
compaction with its spinner. Payload:
`{status: "in_progress" | "completed" |
"failed"}`.
- "external_session_todos"
— internal terminal-observed todo-list
update from the claude-native forwarder.
Caches the list in memory (used by the
snapshot `todos` field) and publishes a
`session.todos` SSE event. Payload:
`{todos: [{content: str, status:
"pending"|"in_progress"|"completed",
activeForm: string}]}`.
Malformed items are silently dropped
before caching/broadcasting.
The route validates `type` against the conversation entity's item
discriminator map plus the documented control/internal event types.
Unknown values fail loud with 400 — they are NOT silently enqueued.
data (object, required)
Type-specific payload. For `"message"`, `{role, content: [...]}`.
For `"interrupt"`, typically `{}`. The route also validates `data`
against the item-type's Pydantic data class for non-interrupt
types (400 on schema mismatch).
202 Accepted
{"queued": true} # regular queued item events
{"queued": false} # "interrupt" and status/control bypasses
{"queued": false, "item_id": "item_..."} # "external_conversation_item"
{"queued": true, "pending_id": "pending_..."} # native-terminal "message" (see below)
400 Bad Request — unknown `type`, or `data` fails the per-type schema
404 Not Found — no session with that id
422 Unprocessable Entity — request body fails Pydantic validation
Native-terminal message events return pending_id. On
claude-native / codex-native sessions a web-composer message is NOT
persisted at POST time (the transcript forwarder is the single writer);
the server records it in an in-memory pending-inputs index and returns
its pending_id. The id is what makes the bubble durable across a
rebind: it (a) re-hydrates from the snapshot's pending_inputs (the
replayed bubble carries the id) and (b) is dropped by id when the
matching session.input.consumed arrives carrying cleared_pending_id.
A client may adopt the id onto its live optimistic bubble for id-based
dedupe; the first-party web client deliberately does NOT (it keeps a
client temp id for React-key stability and relies on a stable key +
FIFO matching), so adoption is optional, not required.
Interrupt is dual-path on purpose. Posting {"type": "interrupt"}
is exposed as an event for API uniformity, but it does NOT enter the
queue — the route invokes the loop's cancel_loop directly so the
interrupt can preempt items already queued in front of it. On every
user-triggered cancel the server emits BOTH response.incomplete
(reason "user_interrupt", from the runtime) AND
session.interrupted (from the route). Co-emitting the
Responses-style event lets off-the-shelf parsers close cleanly while
the session-scoped event carries the cancel intent for session-aware
clients. The internal terminal-observed envelopes also bypass the
queue: external_conversation_item appends/broadcasts an
already-observed item and returns its stored item_id, while
external_output_text_delta publishes a transient
response.output_text.delta event without persisting. Its data is
{delta: string, message_id?: string, index?: integer, final?: boolean}:
delta is required; the optional message_id / index / final are
set only for terminal-observed live streaming (claude-native), where
they let a client scope an in-flight buffer to one assistant message,
order its chunks, and detect the last one (the authoritative final text
still arrives separately via external_conversation_item). They are
omitted for ordinary task streaming, so the published event's wire shape
is unchanged when absent. external_session_status publishes a
session.status event without
creating a task or item. Runner-hosted native sub-agent sessions also
mirror that status into the parent stream as session.child_session.updated
when the child is registered for fan-out. New user-facing event types should
default to the queue.
Resolve Elicitation (URL-based)
POST /v1/sessions/{session_id}/elicitations/{elicitation_id}/resolve
Content-Type: application/json
{"action": "accept"} # or "decline" / "cancel"; optional "content": {...}
202 Accepted
{"queued": false}
404 Not Found — no session with that id
422 Unprocessable Entity — body fails Pydantic validation (e.g. a bad
`action` value)
Delivers a human approval verdict for an outstanding elicitation (one
published as a response.elicitation_request SSE event) to a dedicated,
owner-gated URL. The elicitation_id rides in the URL path; the body is
the MCP ElicitationResult — action plus optional form content.
Requires LEVEL_EDIT on the session.
This is the URL-based counterpart to posting an approval event to
POST .../events: both converge on the same server-side resolver (set
the parked Future, publish response.elicitation_resolved to clear the
pending-elicitation badge, and forward the verdict to the bound runner).
Routing the verdict through this resource-scoped URL keeps human
approval on a dedicated path rather than an in-band session event —
which is what policy ASK gates rely on, so the verdict cannot be
conflated with a generic session event. Any value other than
action: "accept" denies.
Fork Session
POST /v1/sessions/{source_id}/fork
Content-Type: application/json
{
"title": "Exploring alternative approach", // optional
"up_to_response_id": "resp_abc123" // optional
}
Request body matches `SessionForkRequest`:
title (string | null, optional)
Title for the forked session. When null or omitted, the server
derives "Fork of <source_title>".
up_to_response_id (string | null, optional)
Truncation point for the copied history ("fork from this
response"). When set, only items up to and including the last
item of that response are copied; items after it are dropped
from the fork. A truncated fork into a native target rebuilds
its transcript from the truncated items instead of resuming the
source's full native transcript. When null or omitted, the full
history is copied.
201 Created — body matches `SessionResponse` (status "idle",
items are the deep-copied items from the source session).
400 Bad Request — source session is a sub-agent session, has
no agent binding, or up_to_response_id names no response in
the source session
404 Not Found — no session with that source_id, or the source's
agent row is missing
Creates a new session by deep-copying every item from the source
session. The server also clones the source's agent (new agent ID,
same bundle and config) so the fork can be reconfigured independently.
The forked session is not bound to a runner — clients must
PATCH /v1/sessions/{id} with runner_id before posting events,
the same way they bind a runner after resuming an existing session.
The response is a full SessionResponse snapshot of the fork — same
shape as GET /v1/sessions/{id} — with status "idle" and all
copied items in chronological order. Clients must bind a runner
before opening the fork's SSE stream or posting events.
Stream Session
GET /v1/sessions/{session_id}/stream
200 OK
Content-Type: text/event-stream
event: session.status
data: {"type":"session.status","data":{"status":"running"}}
event: session.input.consumed
data: {"type":"session.input.consumed","data":{"queued_item_id":"...","type":"message", ...}}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{...}}
event: response.output_text.delta
data: {"type":"response.output_text.delta","delta":"Hello"}
...
event: response.completed
data: {"type":"response.completed","response":{...}}
event: session.status
data: {"type":"session.status","data":{"status":"idle"}}
data: [DONE]
404 Not Found — no session with that id
Live tail only. No starting_after parameter, no replay of past
events, no sequence numbers exposed on the wire. Reconnecting clients
reconcile via the snapshot endpoint — see "Reconnect Contract".
The stream stays open until the client disconnects or the conversation
is closed; events flow in publish order. Multiple subscribers to the
same session receive the same events. The stream terminates with
data: [DONE]\n\n.
Stream Events
Single source of truth: openapi.json at
the repo root. Every SSE event the server emits to clients is
modeled as a typed Pydantic class in
omnigent/server/schemas.py; the discriminated union
ServerStreamEvent ties them together, and scripts/dump_openapi.py
materializes that union into the OpenAPI 3.2 spec under
components.schemas.ServerStreamEvent (referenced from the SSE
routes via the itemSchema keyword).
The tables below are derived / illustrative — for canonical wire
shapes, field defaults, and schema constraints consult openapi.json
or the per-event Pydantic class docstrings.
Two families coexist on the stream:
Session-scoped (session.*) — wrap the underlying response
stream and surface queue/interrupt semantics.
| Event | Pydantic class | Wire shape (illustrative) |
|---|---|---|
session.status |
SessionStatusEvent |
{type, conversation_id, status: "running" | "waiting" | "idle" | "failed"} |
session.reasoning_effort |
SessionReasoningEffortEvent |
{type, conversation_id, reasoning_effort: string | null} |
session.collaboration_mode |
SessionCollaborationModeEvent |
{type, conversation_id, mode: string} |
session.input.consumed |
SessionInputConsumedEvent |
{type, data: {queued_item_id, type, data, position}} (nested envelope) |
session.interrupted |
SessionInterruptedEvent |
{type, data: {requested_at, queued_item_id?: null}} (nested envelope) |
session.created |
SessionCreatedEvent |
{type, conversation_id: <parent>, child_conversation_id, agent_id, ...} — emitted on the PARENT session's stream when a sub-agent is spawned. |
Note on
session.input.consumed: This event name and payload may change in a future revision; clients should isolate the constant rather than hardcoding it. ImportingSessionInputConsumedEventfromomnigent.server.schemasis the supported pattern.
Response (response.*) — emitted by the executor (and the AP
streaming routes for the lifecycle events). The session stream
multiplexes them; the per-response stream emits them directly.
| Event | Pydantic class |
|---|---|
response.created |
CreatedEvent |
response.queued |
QueuedEvent |
response.in_progress |
InProgressEvent |
response.completed |
CompletedEvent |
response.failed |
FailedEvent |
response.incomplete |
IncompleteEvent |
response.cancelled |
CancelledEvent |
response.output_text.delta |
OutputTextDeltaEvent |
response.output_item.done |
OutputItemDoneEvent |
response.output_file.done |
OutputFileDoneEvent |
response.reasoning.started |
ReasoningStartedEvent |
response.reasoning_text.delta |
ReasoningTextDeltaEvent |
response.reasoning_summary_text.delta |
ReasoningSummaryTextDeltaEvent |
response.retry |
RetryEvent |
response.error |
ErrorEvent |
response.compaction.in_progress |
CompactionInProgressEvent |
response.client_task.cancel |
ClientTaskCancelEvent |
response.heartbeat |
HeartbeatEvent |
response.elicitation_request |
ElicitationRequestEvent |
See the per-class docstring in omnigent/server/schemas.py for
the canonical wire shape and field types of each response.* event.
When a child/sub-agent elicitation is mirrored into an ancestor stream,
response.elicitation_request.params.target_session_id is the child
session whose resolve endpoint must receive the verdict.
Reconnect Contract
The session API has no replay machinery. To reconnect to a session after a disconnect:
- Open the SSE stream (
GET /v1/sessions/{id}/stream). The stream is registered eagerly at session create and survives across turns, so subscribing is safe at any point in the session lifecycle — including before the first turn starts. - GET the snapshot (
GET /v1/sessions/{id}). - Dedupe items between the snapshot and the stream by item id.
Items in
snapshot.itemsthat also appear in stream events are the same item — drop the duplicate. Server-issued IDs are stable.
Opening the stream BEFORE the snapshot is still recommended so no
events fire in the gap, but because the stream queue stays alive
across turns, transient races during reconnect are bounded to the
in-flight HTTP roundtrip rather than the full turn duration. Clients
should rely on session.input.consumed events and item-id dedupe
against the snapshot to reconcile accepted inputs.
Sessions Typical Flow
1. Client creates a session with an uploaded agent bundle
-> POST /v1/sessions multipart {metadata, bundle}
-> 201 {"session_id": "conv_abc123"}
2. Client binds the registered runner
-> PATCH /v1/sessions/{id} {"runner_id":"runner_abc123"}
-> 200 session snapshot with runner_id set
3. Client opens the SSE stream
-> GET /v1/sessions/{id}/stream
-> events flow: session.status -> session.input.consumed
-> response.* deltas -> response.completed -> session.status (idle)
4. Client sends a user message
-> POST /v1/sessions/{id}/events {type:"message", data:{...}}
-> 202 {"queued": true}
-> existing SSE stream emits events for the turn
5. Client loses connection (laptop close, network flap)
-> agent continues processing server-side
6. Client reconnects:
a. Open new GET /v1/sessions/{id}/stream (subscribe FIRST)
b. GET /v1/sessions/{id} (snapshot SECOND)
c. Dedupe items between snapshot and stream by item id
7. Client interrupts a running agent
-> POST /v1/sessions/{id}/events {type:"interrupt"}
-> 202 {"queued": false}
-> stream emits both response.incomplete (reason:user_interrupt)
and session.interrupted, then session.status:idle
Not Yet
PUT /api/agents/{id}— update agent (new bundle)- Stream resumption on GET (sequence_number-based reconnection)
- Authentication
- Rate limiting
- User filtering on conversation list (by metadata, dedicated user field, or auth identity)
- Conversation update metadata (beyond title)
- Search across conversations (full-text search over message content)
- Multi-user identity (
userfield on requests/items to attribute messages in shared conversations) logprobsonoutput_textcontent blocks (optional, used withtop_logprobs)metadataon sessions / tasks — caller-attached key-value pairs (max 16 keys, keys ≤64 chars, values ≤512 chars).purposefield on file uploads (e.g."input","fine-tune")- Audio input (
input_audiocontent type) - Additional output item types:
image_generation_call,web_search_call,file_search_call,code_interpreter_call,mcp_tool_call,computer_call,local_shell_call,apply_patch_call,compaction