4067 lines
169 KiB
Python
4067 lines
169 KiB
Python
"""Pydantic models for the API layer — request/response shapes AND
|
|
SSE stream events.
|
|
|
|
This module is split into two sections, separated by a clearly marked
|
|
delineator further down:
|
|
|
|
1. Request and response body schemas for the JSON endpoints.
|
|
2. SSE event payload models — the discriminated union that every
|
|
event the server emits over its SSE endpoints validates against.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Annotated, Any, Literal, get_args
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, Strict, field_validator, model_validator
|
|
|
|
from omnigent.entities import ConversationItem
|
|
|
|
# ── Shared ──────────────────────────────────────────────────────
|
|
|
|
|
|
class PaginatedList(BaseModel):
|
|
"""
|
|
A paginated list response following cursor-based pagination.
|
|
|
|
:param object: Fixed resource type, always ``"list"``.
|
|
:param data: Page of results. Items are heterogeneous
|
|
(``ResponseObject``, ``ConversationObject``, ``FileObject``,
|
|
or dicts) and list is invariant, so no single concrete type
|
|
satisfies all callers.
|
|
:param first_id: ID of the first item in the page, or ``None``
|
|
if the page is empty, e.g. ``"resp_abc123"``.
|
|
:param last_id: ID of the last item in the page, or ``None``
|
|
if the page is empty, e.g. ``"resp_xyz789"``.
|
|
:param has_more: Whether more items exist beyond this page.
|
|
"""
|
|
|
|
object: str = "list"
|
|
# Any: items are heterogeneous (ResponseObject, ConversationObject,
|
|
# FileObject, or dicts) and list is invariant, so no single concrete
|
|
# type satisfies all callers.
|
|
data: list[Any] = Field(default_factory=list)
|
|
first_id: str | None = None
|
|
last_id: str | None = None
|
|
has_more: bool = False
|
|
|
|
|
|
# ── Agents ──────────────────────────────────────────────────────
|
|
|
|
|
|
class MCPServerSummary(BaseModel):
|
|
"""
|
|
Safe subset of an MCP server's configuration for API exposure.
|
|
|
|
Secret-bearing fields (``headers``, ``env``) are intentionally
|
|
excluded. This model is the wire shape returned inside
|
|
:class:`AgentObject` so clients can display which MCP servers
|
|
an agent is connected to without leaking credentials.
|
|
|
|
:param name: Server name as declared in the agent spec,
|
|
e.g. ``"github"``.
|
|
:param transport: Transport type — ``"stdio"`` or ``"http"``.
|
|
:param description: Optional free-text description from the
|
|
spec, e.g. ``"GitHub MCP server"``. ``None`` when unset.
|
|
:param url: HTTP(S) endpoint URL for ``transport="http"``
|
|
servers, e.g. ``"https://mcp.example.com/sse"``. ``None``
|
|
for stdio servers.
|
|
:param command: Executable path for ``transport="stdio"``
|
|
servers, e.g. ``"uvx"``. ``None`` for http servers.
|
|
:param args: Command-line arguments for ``transport="stdio"``
|
|
servers, e.g. ``["mcp-server-github"]``. Empty list
|
|
when unset.
|
|
"""
|
|
|
|
name: str
|
|
transport: str
|
|
description: str | None = None
|
|
url: str | None = None
|
|
command: str | None = None
|
|
args: list[str] = Field(default_factory=list)
|
|
|
|
|
|
_MCP_SERVER_NAME_RE = r"^[A-Za-z0-9_-][A-Za-z0-9_.-]{0,127}$"
|
|
|
|
|
|
class UpsertMCPServerRequest(BaseModel):
|
|
"""
|
|
Request body for creating or updating a session agent MCP server.
|
|
|
|
Secret-bearing fields (``headers`` and ``env``) are intentionally
|
|
not accepted by the UI route. Existing secrets are preserved when a
|
|
server is edited without changing transport.
|
|
"""
|
|
|
|
name: str = Field(min_length=1, max_length=128, pattern=_MCP_SERVER_NAME_RE)
|
|
transport: Literal["http", "stdio"]
|
|
description: str | None = Field(default=None, max_length=512)
|
|
url: str | None = None
|
|
command: str | None = None
|
|
args: list[str] = Field(default_factory=list, max_length=64)
|
|
|
|
@field_validator("name")
|
|
@classmethod
|
|
def _reject_dot_names(cls, value: str) -> str:
|
|
"""Reject names that would make unsafe or confusing YAML filenames."""
|
|
if value in {".", ".."}:
|
|
raise ValueError("name cannot be '.' or '..'")
|
|
return value
|
|
|
|
@field_validator("args")
|
|
@classmethod
|
|
def _string_args_only(cls, value: list[str]) -> list[str]:
|
|
"""Keep args as a small list of strings."""
|
|
return [str(item) for item in value]
|
|
|
|
@model_validator(mode="after")
|
|
def _validate_transport_fields(self) -> UpsertMCPServerRequest:
|
|
"""Enforce the same transport shape as the agent spec parser."""
|
|
if self.transport == "http":
|
|
if not self.url:
|
|
raise ValueError("url is required when transport is 'http'")
|
|
if not (self.url.startswith("http://") or self.url.startswith("https://")):
|
|
raise ValueError("url must start with http:// or https://")
|
|
if self.command:
|
|
raise ValueError("command is not allowed when transport is 'http'")
|
|
if self.args:
|
|
raise ValueError("args are not allowed when transport is 'http'")
|
|
if self.transport == "stdio":
|
|
if not self.command:
|
|
raise ValueError("command is required when transport is 'stdio'")
|
|
if self.url:
|
|
raise ValueError("url is not allowed when transport is 'stdio'")
|
|
return self
|
|
|
|
|
|
class SkillSummary(BaseModel):
|
|
"""
|
|
Safe subset of a discovered skill for API exposure.
|
|
|
|
Surfaces the skill name and one-line description so clients
|
|
(e.g. the web composer's slash-command menu) can list which
|
|
skills the session has access to. The full skill ``content``
|
|
is intentionally omitted — it's only loaded server-side when
|
|
the harness invokes the skill, and it can be large.
|
|
|
|
:param name: Skill identifier as parsed from the SKILL.md
|
|
frontmatter, e.g. ``"triage-issues"``. Lowercase
|
|
kebab-case.
|
|
:param description: One-line summary from the SKILL.md
|
|
frontmatter, e.g. ``"Triage open GitHub issues in the
|
|
repo."``.
|
|
"""
|
|
|
|
name: str
|
|
description: str
|
|
|
|
|
|
class PolicySummary(BaseModel):
|
|
"""
|
|
Safe subset of a policy's spec for API exposure.
|
|
|
|
Exposes the policy name, type, and phases so the UI can
|
|
display which guardrails are active on an agent. The full
|
|
policy body (prompt text, callable path, label conditions)
|
|
is intentionally excluded — this is a summary for display,
|
|
not a full spec.
|
|
|
|
:param name: Policy name as declared in the agent spec,
|
|
e.g. ``"block_long_sleep"``.
|
|
:param type: Policy type discriminator — ``"function"``
|
|
or ``"prompt"``.
|
|
:param on: List of phase selectors the policy fires on,
|
|
e.g. ``["tool_call"]`` or ``["request", "response"]``.
|
|
:param description: Short detail string about the policy
|
|
implementation. For function policies: the callable
|
|
dotted path. For prompt policies: the first line of
|
|
the prompt. ``None`` when not available.
|
|
"""
|
|
|
|
name: str
|
|
type: str
|
|
on: list[str]
|
|
description: str | None = None
|
|
|
|
|
|
class AgentObject(BaseModel):
|
|
"""
|
|
API representation of a registered agent.
|
|
|
|
:param id: Unique agent identifier, e.g. ``"ag_abc123"``.
|
|
:param object: Fixed resource type, always ``"agent"``.
|
|
:param name: Human-readable agent name,
|
|
e.g. ``"research-agent"``.
|
|
:param version: Monotonic version counter. Starts at 1,
|
|
incremented on each update.
|
|
:param description: Optional free-text description of the
|
|
agent's purpose.
|
|
:param created_at: Unix epoch timestamp of creation.
|
|
:param updated_at: Unix epoch timestamp of the last update,
|
|
or ``None`` if never updated.
|
|
:param harness: The agent's harness/kind, e.g. ``"codex"``,
|
|
``"codex-native"``, or ``"claude-native"`` for
|
|
``executor.type: omnigent`` agents, otherwise the executor
|
|
type (``"claude_sdk"``, ``"agents_sdk"``). ``None`` when the
|
|
bundle cannot be loaded. Lets the Web UI Add Agent picker
|
|
recognise an agent's kind (Codex vs Claude) without
|
|
hardcoding by name slug.
|
|
:param mcp_servers: MCP servers the agent is connected to
|
|
(secret fields omitted). Empty list when the spec
|
|
declares no MCP servers or when the bundle cannot be
|
|
loaded.
|
|
:param mcp_servers_editable: Whether the MCP list can be edited
|
|
through the session UI. Built-in template agents are read-only;
|
|
session-scoped uploaded agents are editable.
|
|
:param policies: Guardrails policies declared on the agent.
|
|
Each entry summarises the policy name, type, and
|
|
phases. Empty list when the spec declares no policies
|
|
or when the bundle cannot be loaded.
|
|
:param skills: Skills bundled in the agent spec
|
|
(``skills/<name>/SKILL.md``). Lets the Web UI's
|
|
new-session composer offer a slash-command menu before a
|
|
session (and its runner) exists. Host-discovered skills
|
|
are runner-owned, so they are NOT listed here — the
|
|
session snapshot's ``skills`` field carries the merged
|
|
set once a runner is bound. Empty list when the spec
|
|
bundles no skills or when the bundle cannot be loaded.
|
|
:param terminals: Terminal names declared in the spec's
|
|
``terminals:`` block, in declaration order, e.g.
|
|
``["shell"]``. The Web UI gates its "new terminal"
|
|
affordance on this list (creation is only offered for
|
|
agents with terminal access) and offers these names as
|
|
the launchable choices. Empty list when the spec
|
|
declares no terminals or when the bundle cannot be
|
|
loaded.
|
|
:param builtin: Whether this is a server-*seeded* built-in
|
|
agent (deterministic, name-derived id) as opposed to an
|
|
operator/user-registered template (random id, e.g. via
|
|
``omnigent server --agent``) or a session-scoped upload.
|
|
The Web UI's new-session picker uses this to decide
|
|
whether a same-named ``omnigent run`` upload may shadow
|
|
the catalog entry: seeded built-ins are protected, while
|
|
a user-registered template is superseded by a newer
|
|
same-named upload. Always ``False`` for session-scoped
|
|
agents.
|
|
"""
|
|
|
|
id: str
|
|
object: str = "agent"
|
|
name: str
|
|
version: int = 1
|
|
description: str | None = None
|
|
created_at: int
|
|
updated_at: int | None = None
|
|
harness: str | None = None
|
|
mcp_servers: list[MCPServerSummary] = Field(default_factory=list)
|
|
mcp_servers_editable: bool = False
|
|
policies: list[PolicySummary] = Field(default_factory=list)
|
|
skills: list[SkillSummary] = Field(default_factory=list)
|
|
terminals: list[str] = Field(default_factory=list)
|
|
builtin: bool = False
|
|
|
|
|
|
# ── Session Policies ───────────────────────────────────────────
|
|
|
|
|
|
class SessionPolicyObject(BaseModel):
|
|
"""
|
|
API representation of a session-scoped policy.
|
|
|
|
Returned by all CRUD endpoints under
|
|
``/v1/sessions/{session_id}/policies``.
|
|
|
|
:param id: Opaque policy identifier, e.g. ``"spol_abc123"``.
|
|
``None`` for spec-declared policies that are not
|
|
store-persisted.
|
|
:param object: Fixed resource type, always
|
|
``"session.policy"``.
|
|
:param name: Human-readable policy name,
|
|
e.g. ``"block_non_feature_branch_push"``.
|
|
:param type: Handler discriminator: ``"python"`` or
|
|
``"url"``.
|
|
:param handler: Dotted import path (python) or HTTP URL
|
|
(url), e.g. ``"github_mcp_policy.block_push"`` or
|
|
``"https://example.com/policies/eval"``.
|
|
:param factory_params: Dict of kwargs passed to the handler
|
|
when it is a factory function. ``None`` for direct
|
|
callables and ``type="url"`` handlers.
|
|
:param enabled: Whether the engine consults this policy.
|
|
:param source: Origin of the policy: ``"session"`` for
|
|
CRUD-created policies, ``"spec"`` for policies
|
|
declared in the agent YAML. Spec policies cannot be
|
|
patched or deleted.
|
|
:param created_at: Unix epoch timestamp of creation.
|
|
:param updated_at: Unix epoch timestamp of the last
|
|
update, or ``None`` if never updated.
|
|
"""
|
|
|
|
id: str | None
|
|
object: str = "session.policy"
|
|
name: str
|
|
type: str
|
|
handler: str
|
|
factory_params: dict[str, Any] | None = None
|
|
enabled: bool = True
|
|
source: str = "session"
|
|
created_at: int
|
|
updated_at: int | None = None
|
|
|
|
|
|
_DOTTED_PATH_RE = r"^[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)+$"
|
|
|
|
|
|
class CreateSessionPolicyRequest(BaseModel):
|
|
"""
|
|
Request body for ``POST /v1/sessions/{session_id}/policies``.
|
|
|
|
:param name: Human-readable policy name. Must be unique
|
|
within the session, e.g.
|
|
``"block_non_feature_branch_push"``.
|
|
:param type: Handler discriminator: ``"python"`` or
|
|
``"url"``.
|
|
:param handler: Dotted import path (python) or HTTPS URL
|
|
(url), e.g.
|
|
``"github_mcp_policy.block_non_misc_push"``
|
|
or ``"https://example.com/policies/eval"``.
|
|
:param factory_params: Optional dict of kwargs passed to the
|
|
handler when it is a factory function. Only valid for
|
|
``type="python"``, e.g. ``{"limit": 10}``.
|
|
"""
|
|
|
|
name: str
|
|
type: str
|
|
handler: str
|
|
factory_params: dict[str, Any] | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def _validate_type_and_handler(self) -> CreateSessionPolicyRequest:
|
|
"""Reject unknown policy types and validate handler format.
|
|
|
|
For ``type="url"``, requires an ``https://`` URL.
|
|
For ``type="python"``, requires a valid dotted import path
|
|
(at least two segments, e.g. ``"pkg.module"``).
|
|
|
|
:returns: The validated request unchanged.
|
|
:raises ValueError: If ``type`` is invalid, or ``handler``
|
|
does not match the expected format for the type.
|
|
"""
|
|
if self.type not in ("python", "url"):
|
|
raise ValueError(f"type must be 'python' or 'url', got '{self.type}'")
|
|
if self.type == "url":
|
|
if not self.handler.startswith("https://"):
|
|
raise ValueError("handler must be an https:// URL for type 'url'")
|
|
elif self.type == "python":
|
|
if not re.match(_DOTTED_PATH_RE, self.handler):
|
|
raise ValueError(
|
|
"handler must be a valid dotted import path "
|
|
"(e.g. 'pkg.module.func') for type 'python'"
|
|
)
|
|
return self
|
|
|
|
|
|
class UpdateSessionPolicyRequest(BaseModel):
|
|
"""
|
|
Request body for ``PATCH /v1/sessions/{session_id}/policies/{policy_id}``.
|
|
|
|
All fields are optional; ``None`` fields are left unchanged.
|
|
Unknown fields (including ``type``, which is immutable) are
|
|
rejected with ``422``.
|
|
|
|
:param name: New policy name. ``None`` leaves it unchanged.
|
|
:param handler: New handler path or URL. ``None`` leaves it
|
|
unchanged.
|
|
:param enabled: New enabled flag. ``None`` leaves it
|
|
unchanged.
|
|
"""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str | None = None
|
|
handler: str | None = None
|
|
enabled: bool | None = None
|
|
|
|
|
|
# ── Default Policies ──────────────────────────────────────────────
|
|
|
|
|
|
class DefaultPolicyObject(BaseModel):
|
|
"""
|
|
API representation of a server-wide default policy.
|
|
|
|
Returned by all CRUD endpoints under ``/v1/policies``.
|
|
|
|
:param id: Opaque policy identifier, e.g. ``"dpol_abc123"``.
|
|
:param object: Fixed resource type, always
|
|
``"default_policy"``.
|
|
:param name: Human-readable policy name,
|
|
e.g. ``"block_non_feature_branch_push"``.
|
|
:param type: Handler discriminator: ``"python"`` or
|
|
``"url"``.
|
|
:param handler: Dotted import path (python) or HTTP URL
|
|
(url), e.g. ``"github_mcp_policy.block_push"`` or
|
|
``"https://example.com/policies/eval"``.
|
|
:param factory_params: Dict of kwargs passed to the handler
|
|
when it is a factory function. ``None`` for direct
|
|
callables and ``type="url"`` handlers.
|
|
:param enabled: Whether the engine consults this policy.
|
|
:param created_at: Unix epoch timestamp of creation.
|
|
:param updated_at: Unix epoch timestamp of the last
|
|
update, or ``None`` if never updated.
|
|
:param created_by: User ID of the admin who created this
|
|
policy, or ``None`` in single-user mode.
|
|
"""
|
|
|
|
id: str
|
|
object: str = "default_policy"
|
|
name: str
|
|
type: str
|
|
handler: str
|
|
factory_params: dict[str, Any] | None = None
|
|
enabled: bool = True
|
|
created_at: int
|
|
updated_at: int | None = None
|
|
created_by: str | None = None
|
|
|
|
|
|
class CreateDefaultPolicyRequest(BaseModel):
|
|
"""
|
|
Request body for ``POST /v1/policies``.
|
|
|
|
:param name: Human-readable policy name. Must be globally
|
|
unique, e.g. ``"block_non_feature_branch_push"``.
|
|
:param type: Handler discriminator: ``"python"``, ``"url"``,
|
|
:param handler: Dotted import path (python) or HTTPS URL
|
|
(url), e.g.
|
|
``"github_mcp_policy.block_non_misc_push"``
|
|
or ``"https://example.com/policies/eval"``.
|
|
:param factory_params: Optional dict of kwargs passed to the
|
|
handler when it is a factory function. Only valid for
|
|
``type="python"``, e.g. ``{"limit": 10}``.
|
|
"""
|
|
|
|
name: str
|
|
type: str
|
|
handler: str
|
|
factory_params: dict[str, Any] | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def _validate_type_and_handler(self) -> CreateDefaultPolicyRequest:
|
|
"""Reject unknown policy types and validate handler format.
|
|
|
|
Same validation rules as :class:`CreateSessionPolicyRequest`.
|
|
|
|
:returns: The validated request unchanged.
|
|
:raises ValueError: If ``type`` is invalid, or ``handler``
|
|
does not match the expected format for the type.
|
|
"""
|
|
if self.type not in ("python", "url"):
|
|
raise ValueError(f"type must be 'python' or 'url', got '{self.type}'")
|
|
if self.type == "url":
|
|
if not self.handler.startswith("https://"):
|
|
raise ValueError("handler must be an https:// URL for type 'url'")
|
|
elif self.type == "python":
|
|
if not re.match(_DOTTED_PATH_RE, self.handler):
|
|
raise ValueError(
|
|
"handler must be a valid dotted import path "
|
|
"(e.g. 'pkg.module.func') for type 'python'"
|
|
)
|
|
return self
|
|
|
|
|
|
class UpdateDefaultPolicyRequest(BaseModel):
|
|
"""
|
|
Request body for ``PATCH /v1/policies/{policy_id}``.
|
|
|
|
All fields are optional; ``None`` fields are left unchanged.
|
|
Unknown fields (including ``type``, which is immutable) are
|
|
rejected with ``422``.
|
|
|
|
:param name: New policy name. ``None`` leaves it unchanged.
|
|
:param handler: New handler path or URL. ``None`` leaves it
|
|
unchanged.
|
|
:param enabled: New enabled flag. ``None`` leaves it
|
|
unchanged.
|
|
"""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str | None = None
|
|
handler: str | None = None
|
|
enabled: bool | None = None
|
|
|
|
|
|
# ── Files ───────────────────────────────────────────────────────
|
|
|
|
|
|
class FileObject(BaseModel):
|
|
"""
|
|
API representation of an uploaded file.
|
|
|
|
:param id: Unique file identifier, e.g. ``"file_abc123"``.
|
|
:param object: Fixed resource type, always ``"file"``.
|
|
:param filename: Original filename, e.g. ``"report.pdf"``.
|
|
:param bytes: File size in bytes.
|
|
:param created_at: Unix epoch timestamp of upload.
|
|
"""
|
|
|
|
id: str
|
|
object: str = "file"
|
|
filename: str
|
|
bytes: int
|
|
created_at: int
|
|
|
|
|
|
class CopyFilesRequest(BaseModel):
|
|
"""
|
|
Request to copy files from a lineage ancestor into a session.
|
|
|
|
The destination session is the path parameter; ``source_session_id``
|
|
must be a STRICT ancestor of the destination up its
|
|
``parent_conversation_id`` chain (spawn lineage) — the destination may
|
|
not name itself as the source. The copy creates new child-scoped rows —
|
|
it does not grant cross-session read access.
|
|
|
|
:param source_session_id: Session that owns the source files, e.g.
|
|
``"conv_parent"``. Must be a strict ancestor of the destination.
|
|
:param file_ids: Non-empty, unique ids of the source-owned files to
|
|
copy, e.g. ``["file_abc123"]``.
|
|
"""
|
|
|
|
source_session_id: str
|
|
file_ids: list[Annotated[str, Field(min_length=1)]] = Field(
|
|
min_length=1,
|
|
json_schema_extra={"uniqueItems": True},
|
|
)
|
|
|
|
|
|
class CopiedFile(BaseModel):
|
|
"""
|
|
A single copied file's new identity and preserved metadata.
|
|
|
|
:param new_id: The new child-scoped file id, e.g. ``"file_def456"``.
|
|
:param filename: The copied file's name, carried over from the source.
|
|
:param content_type: The copied file's MIME type, preserved from the
|
|
source row so the caller need not re-fetch it or guess from the
|
|
filename. ``None`` when the source row had no recorded type.
|
|
"""
|
|
|
|
new_id: str
|
|
filename: str
|
|
content_type: str | None = None
|
|
|
|
|
|
class CopyFilesResponse(BaseModel):
|
|
"""
|
|
Result of a lineage-scoped file copy.
|
|
|
|
:param object: Fixed type, always ``"session.files.copied"``.
|
|
:param session_id: Destination session that now owns the copies.
|
|
:param mapping: Map of source ``file_id`` to the copied file's new
|
|
identity and preserved metadata (id, filename, content type), so a
|
|
caller can attach the copy without a follow-up metadata fetch.
|
|
"""
|
|
|
|
object: str = "session.files.copied"
|
|
session_id: str
|
|
mapping: dict[str, CopiedFile]
|
|
|
|
|
|
# ── Session Resources ───────────────────────────────────────────
|
|
|
|
|
|
class SessionResourceObject(BaseModel):
|
|
"""
|
|
API representation of a session-scoped resource handle.
|
|
|
|
:param id: Opaque resource identifier, e.g. ``"default"`` or
|
|
``"terminal_bash_s1"``.
|
|
:param object: Fixed resource type, always ``"session.resource"``.
|
|
:param type: Resource kind, initially ``"environment"``,
|
|
``"terminal"``, or ``"file"``.
|
|
:param session_id: Owning session/conversation id.
|
|
:param name: Human-readable display name. Not required to be
|
|
globally unique.
|
|
:param metadata: Resource-type-specific metadata.
|
|
:param environment: For terminal resources, the environment id the
|
|
terminal actually runs in. Omitted for non-terminal resources.
|
|
"""
|
|
|
|
id: str
|
|
object: Literal["session.resource"]
|
|
type: Literal["environment", "terminal", "file"]
|
|
session_id: str
|
|
name: str
|
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
environment: str | None = None
|
|
|
|
model_config = ConfigDict(extra="forbid", strict=True)
|
|
|
|
|
|
class SessionResourceListPage(BaseModel):
|
|
"""Strict runner resource-list wire contract."""
|
|
|
|
object: Literal["list"]
|
|
data: list[SessionResourceObject]
|
|
first_id: str | None
|
|
last_id: str | None
|
|
has_more: bool
|
|
|
|
model_config = ConfigDict(extra="forbid", strict=True)
|
|
|
|
|
|
class SessionResourcePaginatedList(BaseModel):
|
|
"""Public paginated list of session resources."""
|
|
|
|
object: Literal["list"] = "list"
|
|
data: list[SessionResourceObject] = Field(default_factory=list)
|
|
first_id: str | None = None
|
|
last_id: str | None = None
|
|
has_more: bool = False
|
|
|
|
|
|
# ── Conversations ───────────────────────────────────────────────
|
|
|
|
|
|
class ConversationObject(BaseModel):
|
|
"""
|
|
API representation of a conversation.
|
|
|
|
:param id: Unique conversation identifier,
|
|
e.g. ``"conv_abc123"``.
|
|
:param object: Fixed resource type, always
|
|
``"conversation"``.
|
|
:param title: Optional user-assigned conversation title.
|
|
:param created_at: Unix epoch timestamp of creation.
|
|
:param updated_at: Unix epoch timestamp of the last
|
|
update, e.g. ``1774118400``.
|
|
:param labels: Session-scoped guardrails labels, mirroring
|
|
the runtime ``Conversation.labels`` dict. Empty dict when
|
|
the PolicyEngine hasn't written any labels yet. Exposed so
|
|
the REPL's Ctrl+O debug overlay can render them at parity
|
|
with the legacy ``omnigent run`` Ctrl+G overview.
|
|
"""
|
|
|
|
id: str
|
|
object: str = "conversation"
|
|
title: str | None = None
|
|
created_at: int
|
|
updated_at: int
|
|
labels: dict[str, str] = Field(default_factory=dict)
|
|
|
|
|
|
class ConversationDeleted(BaseModel):
|
|
"""
|
|
Confirmation payload returned after deleting a conversation.
|
|
|
|
:param id: ID of the deleted conversation,
|
|
e.g. ``"conv_abc123"``.
|
|
:param object: Fixed resource type, always
|
|
``"conversation.deleted"``.
|
|
:param deleted: Always ``True``.
|
|
"""
|
|
|
|
id: str
|
|
object: str = "conversation.deleted"
|
|
deleted: bool = True
|
|
|
|
|
|
class ConversationRef(BaseModel):
|
|
"""
|
|
Lightweight reference to a conversation, used in request and
|
|
response bodies where only the conversation ID is needed.
|
|
|
|
:param id: Conversation identifier, e.g. ``"conv_abc123"``.
|
|
"""
|
|
|
|
id: str
|
|
|
|
|
|
class ChildSessionSummary(BaseModel):
|
|
"""
|
|
Summary of a sub-agent (child) session under a parent session.
|
|
|
|
Powers ``GET /v1/sessions/{id}/child_sessions``. Lets the web /
|
|
REPL debug surface enumerate sub-agent calls spawned from a
|
|
parent session without parsing parent ``function_call_output``
|
|
JSON handles (the legacy TUI Ctrl+O path). The endpoint is the
|
|
canonical "historical truth" source; the existing transient
|
|
``session.created`` SSE event handles live incremental updates.
|
|
|
|
Fields are derived from the child :class:`Conversation` plus its
|
|
latest :class:`Task` (newest by ``created_at``).
|
|
|
|
:param id: Child conversation/session identifier,
|
|
e.g. ``"conv_child123"``.
|
|
:param object: Fixed resource type, always
|
|
``"child_session"``.
|
|
:param parent_session_id: Parent conversation id (echo of the
|
|
route's ``session_id`` path parameter), e.g.
|
|
``"conv_parent987"``. Stable join key for clients that
|
|
cache child rows across multiple parents.
|
|
:param title: Sub-agent title, ``"{agent_type}:{session_name}"``
|
|
as written by :func:`omnigent.tools.builtins.spawn._spawn_one`,
|
|
e.g. ``"researcher:auth"``. ``None`` only for legacy /
|
|
malformed rows; the spawn path always sets it.
|
|
:param tool: UI-facing sub-agent label. For Omnigent-spawned
|
|
children this is derived from the prefix of ``title`` before
|
|
the first ``":"``, e.g. ``"researcher"``. For Codex-native
|
|
children this is the Codex-assigned ``agent_nickname`` when
|
|
available, then ``agent_role``, then ``"Codex"``. Falls back
|
|
to the raw title for legacy / malformed rows; ``None`` only
|
|
when ``title`` itself is ``None`` or empty.
|
|
:param session_name: Sub-agent instance name, the suffix of
|
|
``title`` after the first ``":"``, e.g. ``"auth"``. ``None``
|
|
if ``title`` is ``None`` or missing a colon.
|
|
:param kind: Conversation kind discriminator, always
|
|
``"sub_agent"`` for rows surfaced by this endpoint.
|
|
:param created_at: Unix epoch timestamp of child creation.
|
|
:param updated_at: Unix epoch timestamp of the child's most
|
|
recent update.
|
|
:param agent_id: Agent id recorded on the latest task,
|
|
e.g. ``"ag_abc123"``. ``None`` if the child has no tasks
|
|
yet (rare — ``_spawn_one`` creates a task atomically with
|
|
the conversation).
|
|
:param agent_name: Agent type recorded on the latest task,
|
|
e.g. ``"researcher"``. Mirrors the ``tool`` prefix in
|
|
``title`` and is provided alongside it because the title
|
|
is a denormalized string while ``agent_name`` is the
|
|
durable per-task value.
|
|
:param current_task_id: Latest task id for the child
|
|
(newest by ``created_at``), e.g. ``"task_abc123"``.
|
|
``None`` if no tasks exist.
|
|
:param current_task_status: Status of the latest task,
|
|
e.g. ``"completed"``, ``"in_progress"``, ``"failed"``.
|
|
``None`` if no tasks exist.
|
|
:param busy: ``True`` when the child's session loop is live.
|
|
Mirrors the algorithm used by ``GET /v1/sessions/{id}`` to
|
|
compute ``status``: read the live in-memory cache first
|
|
(``"running"``/``"waiting"`` → busy), and fall back to the
|
|
latest task's status on cache miss (``"queued"`` /
|
|
``"in_progress"`` → busy). For NO_DBOS sessions the tasks
|
|
table is not populated during active runs, so the cache
|
|
consult is what keeps the rail's "Working" badge correct.
|
|
:param labels: Session-scoped guardrails labels on the child
|
|
conversation (mirrors :class:`ConversationObject.labels`).
|
|
:param last_task_error: Error details from the child's most recent
|
|
failed run, e.g.
|
|
``{"code": "required_terminal_exited", "message": "..."}``.
|
|
``None`` when the child has no durable failure detail. This is
|
|
the typed projection of runner-owned failure labels; clients
|
|
should not parse those labels directly.
|
|
:param last_message_preview: Single-line preview of the most
|
|
recent message item in the child's conversation, truncated
|
|
to ~150 chars with a trailing ellipsis when longer. ``None``
|
|
when the child has no message items yet (rare — the spawn
|
|
tool immediately commits a user message). Lets the UI
|
|
render a real-time "what's the sub-agent saying right now"
|
|
line without fetching the child's full item history.
|
|
:param pending_elicitations_count: Number of approval / input
|
|
prompts the child is currently blocked on, read from the
|
|
server's :mod:`omnigent.runtime.pending_elicitations`
|
|
index. ``> 0`` means the sub-agent is parked awaiting user
|
|
input — the Agents rail renders an "awaiting input" badge so
|
|
a fanned-out sub-agent that needs attention is visible
|
|
without opening its chat. Mirrors
|
|
:attr:`SessionListItem.pending_elicitations_count`.
|
|
"""
|
|
|
|
id: str
|
|
object: str = "child_session"
|
|
parent_session_id: str
|
|
title: str | None = None
|
|
tool: str | None = None
|
|
session_name: str | None = None
|
|
kind: str = "sub_agent"
|
|
created_at: int
|
|
updated_at: int
|
|
agent_id: str | None = None
|
|
agent_name: str | None = None
|
|
current_task_id: str | None = None
|
|
current_task_status: str | None = None
|
|
busy: bool = False
|
|
labels: dict[str, str] = Field(default_factory=dict)
|
|
last_task_error: dict[str, str] | None = None
|
|
last_message_preview: str | None = None
|
|
pending_elicitations_count: int = 0
|
|
|
|
|
|
# ── Responses ───────────────────────────────────────────────────
|
|
|
|
|
|
class UsageDetails(BaseModel):
|
|
"""
|
|
Breakdown of output token usage.
|
|
|
|
:param reasoning_tokens: Number of tokens consumed by
|
|
chain-of-thought reasoning.
|
|
"""
|
|
|
|
reasoning_tokens: int = 0
|
|
|
|
|
|
class Usage(BaseModel):
|
|
"""
|
|
Token usage statistics for a response.
|
|
|
|
:param input_tokens: Number of input (prompt) tokens consumed.
|
|
:param output_tokens: Number of output (completion) tokens
|
|
generated.
|
|
:param output_tokens_details: Breakdown of output token usage
|
|
(e.g. reasoning tokens).
|
|
:param total_tokens: Sum of input and output tokens across all
|
|
LLM sub-calls for this turn (billing total).
|
|
:param context_tokens: Context-fill estimate for the next turn —
|
|
set only by executors that make multiple LLM sub-calls per
|
|
turn (e.g. ``openai-agents``). For single-call executors
|
|
this is absent and ``total_tokens`` serves the same purpose.
|
|
The toolbar context ring and ``/context`` command use this
|
|
field when present, falling back to ``total_tokens``.
|
|
:param cache_read_input_tokens: Prompt tokens served from a
|
|
provider prompt cache (cache hit), billed at a reduced rate.
|
|
Reported by Anthropic-style providers as a count *separate*
|
|
from ``input_tokens`` (which carries only the non-cached
|
|
portion); ``0`` when the provider does not break out cache
|
|
usage. Consumed by the cache-aware server-side cost path.
|
|
:param cache_creation_input_tokens: Prompt tokens written to the
|
|
provider prompt cache (cache creation), billed at a premium
|
|
rate. Like ``cache_read_input_tokens``, this is separate from
|
|
``input_tokens``; ``0`` when not reported.
|
|
:param model: The LLM model the harness actually used for this
|
|
turn, e.g. ``"claude-opus-4-8"`` or ``"databricks-gpt-5-5"``.
|
|
Reported by relay executors so the server-side cost path can
|
|
price the turn even when the agent spec pins no ``llm.model``
|
|
(e.g. supervisors that delegate / use the harness default).
|
|
``None`` when the executor doesn't report it; the cost path
|
|
then falls back to the session override / spec model.
|
|
:param cost_usd: Authoritative per-turn cost in USD reported
|
|
directly by the harness/provider (e.g. GitHub Copilot's
|
|
AI-credit total). When present, the server-side cost path uses
|
|
it in preference to the catalog token-price estimate; ``None``
|
|
when the harness doesn't report a cost (the common case, where
|
|
cost is computed from token counts x catalog pricing).
|
|
"""
|
|
|
|
input_tokens: int = 0
|
|
output_tokens: int = 0
|
|
output_tokens_details: UsageDetails = Field(default_factory=UsageDetails)
|
|
total_tokens: int = 0
|
|
context_tokens: int | None = None
|
|
cache_read_input_tokens: int = 0
|
|
cache_creation_input_tokens: int = 0
|
|
model: str | None = None
|
|
cost_usd: float | None = None
|
|
|
|
|
|
class ErrorDetail(BaseModel):
|
|
"""
|
|
Machine-readable error information attached to a failed response.
|
|
|
|
:param code: Error code string, e.g. ``"server_error"``,
|
|
``"invalid_input"``.
|
|
:param message: Human-readable error description.
|
|
"""
|
|
|
|
code: str
|
|
message: str
|
|
|
|
|
|
class IncompleteDetails(BaseModel):
|
|
"""
|
|
Details explaining why a response is incomplete.
|
|
|
|
:param reason: Reason the response stopped early, e.g.
|
|
``"max_output_tokens"``, ``"max_tool_calls"``.
|
|
"""
|
|
|
|
reason: str
|
|
|
|
|
|
class CreateResponseRequest(BaseModel):
|
|
"""
|
|
Internal request body the harness scaffold builds for each turn.
|
|
|
|
Originally the ``POST /v1/responses`` request schema; that route
|
|
was removed but the harness scaffold still synthesizes this shape
|
|
internally to drive an executor turn.
|
|
|
|
:param model: Agent name to invoke, e.g.
|
|
``"research-agent"``. Must match a registered agent.
|
|
:param input: User input — either a plain string (converted
|
|
to a single ``input_text`` block) or a list of content
|
|
blocks, e.g.
|
|
``[{"type": "input_text", "text": "Hello"}]``.
|
|
:param stream: If ``True``, return an SSE stream instead of
|
|
blocking until completion.
|
|
:param background: If ``True``, the task runs in the
|
|
background and the caller may poll for results.
|
|
:param store: Must be ``True`` (persisted responses). The
|
|
server rejects ``False``.
|
|
:param instructions: Per-request system instructions that
|
|
override the agent's default instructions.
|
|
:param previous_response_id: ID of the prior response in the
|
|
conversation thread, e.g. ``"resp_abc123"``. Enables
|
|
multi-turn continuation and steering.
|
|
:param conversation: Explicit conversation reference for
|
|
fork validation. Must match the conversation that owns
|
|
``previous_response_id``.
|
|
:param reasoning: Reasoning configuration,
|
|
e.g. ``{"effort": "medium"}``.
|
|
:param model_override: Optional per-request LLM model override,
|
|
e.g. ``"openai/gpt-5.4-mini"``. Distinct from ``model``
|
|
(agent name). Substitutes for the spec's ``llm.model`` for
|
|
this single request. Drives the REPL's ``/model`` command.
|
|
:param context_management: Compaction strategy objects,
|
|
e.g. ``[{"type": "compaction", ...}]``.
|
|
:param temperature: Ignored — agent controls this. Silently
|
|
dropped.
|
|
:param top_p: Ignored — agent controls this. Silently
|
|
dropped.
|
|
:param tools: Optional list of client-specified tools in standard
|
|
OpenAI function format. When the LLM invokes one, the
|
|
``function_call`` output items are returned to the caller (the
|
|
response completes) rather than being executed server-side. The
|
|
caller handles execution and continues via
|
|
``previous_response_id``. Returns 400 if any entry is malformed
|
|
or missing ``function.name``, e.g.
|
|
``[{"type": "function", "function": {"name": "get_weather",
|
|
"description": "...", "parameters": {...}}}]``.
|
|
:param tool_choice: Ignored — agent controls this. Silently
|
|
dropped.
|
|
:param max_output_tokens: Ignored — agent controls this.
|
|
Silently dropped.
|
|
:param frequency_penalty: Ignored — agent controls this.
|
|
Silently dropped.
|
|
:param presence_penalty: Ignored — agent controls this.
|
|
Silently dropped.
|
|
:param parallel_tool_calls: Ignored — agent controls this.
|
|
Silently dropped.
|
|
:param max_tool_calls: Ignored — agent controls this.
|
|
Silently dropped.
|
|
:param top_logprobs: Ignored — agent controls this. Silently
|
|
dropped.
|
|
"""
|
|
|
|
# Optional when previous_response_id is set; server resolves the agent
|
|
# from the prior task. Required for fresh conversations (no prior task).
|
|
model: str | None = None
|
|
# Heterogeneous content blocks (input_text, input_image, input_file)
|
|
# or a plain string shorthand; shape varies by block type.
|
|
input: str | list[dict[str, Any]]
|
|
stream: bool = False
|
|
background: bool = False
|
|
store: bool = True
|
|
instructions: str | None = None
|
|
previous_response_id: str | None = None
|
|
# Correlation id for a mid-turn injection (RUNNER_MESSAGE_INGEST.md
|
|
# Part B). Stamped by the runner when it forwards a buffered message
|
|
# as a live injection; echoed back by the executor adapter in an
|
|
# ``injection.consumed`` marker once the executor actually consumes
|
|
# the message, so the runner can drop the buffered copy and not
|
|
# re-deliver it in a continuation turn. ``None`` for fresh turns.
|
|
injection_id: str | None = None
|
|
conversation: ConversationRef | None = None
|
|
# Reasoning config, e.g. {"effort": "low"|"medium"|"high"}
|
|
reasoning: dict[str, str] | None = None
|
|
# Per-request LLM model override (distinct from ``model``, which
|
|
# carries the agent name). See class docstring for semantics.
|
|
model_override: str | None = None
|
|
# Compaction strategy objects, e.g. [{"type": "compaction", ...}]
|
|
context_management: list[dict[str, Any]] | None = None
|
|
# Ignored fields — agent controls these; silently dropped.
|
|
# Typed loosely because we only need to accept and discard them.
|
|
temperature: float | None = None
|
|
top_p: float | None = None
|
|
tools: list[dict[str, Any]] | None = None
|
|
tool_choice: dict[str, Any] | str | None = None
|
|
max_output_tokens: int | None = None
|
|
frequency_penalty: float | None = None
|
|
presence_penalty: float | None = None
|
|
parallel_tool_calls: bool | None = None
|
|
max_tool_calls: int | None = None
|
|
top_logprobs: int | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def _require_model_for_new_conversations(self) -> CreateResponseRequest:
|
|
"""
|
|
Enforce that ``model`` is provided when starting a fresh conversation.
|
|
|
|
When ``previous_response_id`` is not set the server has no prior task
|
|
from which to resolve the agent, so ``model`` is required. Omitting it
|
|
produces a 422 at the API boundary rather than a cryptic runtime error
|
|
deep in the route handler.
|
|
|
|
:returns: ``self`` unchanged when the invariant holds.
|
|
:raises ValueError: When ``model`` is ``None`` and
|
|
``previous_response_id`` is not set.
|
|
"""
|
|
if self.model is None and not self.previous_response_id:
|
|
raise ValueError("model is required when previous_response_id is not set")
|
|
return self
|
|
|
|
|
|
class ResponseObject(BaseModel):
|
|
"""
|
|
API representation of a response (task execution result).
|
|
|
|
:param id: Unique response identifier, e.g.
|
|
``"resp_abc123"``.
|
|
:param object: Fixed resource type, always ``"response"``.
|
|
:param status: Lifecycle status, one of ``"queued"``,
|
|
``"in_progress"``, ``"completed"``, ``"failed"``,
|
|
``"incomplete"``, ``"cancelled"``.
|
|
:param model: Agent name that produced this response,
|
|
e.g. ``"research-agent"``.
|
|
:param created_at: Unix epoch timestamp of creation.
|
|
:param completed_at: Unix epoch timestamp of completion, or
|
|
``None`` if not yet complete.
|
|
:param output: Heterogeneous output items (messages,
|
|
reasoning, function_calls) serialized as dicts; shape
|
|
varies by item type. Empty for non-completed responses.
|
|
:param background: Whether this response was created as a
|
|
background task.
|
|
:param store: Whether this response is persisted. Always
|
|
``True``.
|
|
:param usage: Token usage statistics, or ``None`` if not
|
|
yet available.
|
|
:param previous_response_id: ID of the prior response in
|
|
the conversation thread, or ``None`` for the first turn.
|
|
:param conversation: Reference to the owning conversation.
|
|
:param instructions: Per-request system instructions
|
|
override, or ``None``.
|
|
:param reasoning: Reasoning configuration,
|
|
e.g. ``{"effort": "medium"}``.
|
|
:param error: Error details if the response failed.
|
|
:param incomplete_details: Details if the response is
|
|
incomplete (e.g. hit token limit).
|
|
"""
|
|
|
|
id: str
|
|
object: str = "response"
|
|
status: str
|
|
model: str
|
|
created_at: int
|
|
completed_at: int | None = None
|
|
# Heterogeneous output items (messages, reasoning, function_calls);
|
|
# shape varies by item type.
|
|
output: list[dict[str, Any]] = Field(default_factory=list)
|
|
background: bool = False
|
|
store: bool = True
|
|
usage: Usage | None = None
|
|
previous_response_id: str | None = None
|
|
conversation: ConversationRef | None = None
|
|
instructions: str | None = None
|
|
reasoning: dict[str, str] | None = None
|
|
error: ErrorDetail | None = None
|
|
incomplete_details: IncompleteDetails | None = None
|
|
|
|
|
|
class ToolResult(BaseModel):
|
|
"""
|
|
A single tool result submitted by the client via PATCH.
|
|
|
|
:param call_id: The tool call ID that this result
|
|
corresponds to, e.g. ``"call_abc123"``.
|
|
:param output: The tool's string output,
|
|
e.g. ``'["paper1.pdf", "paper2.pdf"]'``.
|
|
"""
|
|
|
|
call_id: str
|
|
output: str
|
|
|
|
|
|
class ElicitationResult(BaseModel):
|
|
"""
|
|
Consumer reply to an outstanding elicitation.
|
|
|
|
Field names + semantics mirror MCP's ``ElicitResult`` verbatim.
|
|
Omnigent clients deliver this shape inside the session-scoped
|
|
``approval`` event body, alongside the ``elicitation_id``
|
|
correlation key.
|
|
|
|
:param action: User action per MCP semantics. ``"accept"`` =
|
|
approved (form submitted / confirmation given).
|
|
``"decline"`` = explicit refusal. ``"cancel"`` = dismissed
|
|
without an explicit choice (also the verdict the server
|
|
synthesizes on elicitation timeout).
|
|
:param content: Form data when ``action == "accept"`` and the
|
|
``requestedSchema`` had fields. ``None`` (or omitted) for
|
|
binary approve/reject elicitations and for ``decline`` /
|
|
``cancel`` actions. Values are restricted to JSON scalars
|
|
and string lists per the MCP spec.
|
|
"""
|
|
|
|
action: Literal["accept", "decline", "cancel"]
|
|
# ``str | int | float | bool | list[str] | None`` mirrors MCP's
|
|
# ElicitResult.content value type — keep them aligned so an MCP
|
|
# client can bridge to our endpoint without translation.
|
|
content: dict[str, str | int | float | bool | list[str] | None] | None = None
|
|
|
|
|
|
# ── Sessions (/v1/sessions) ────────────────────────────────────
|
|
|
|
|
|
class SessionEventInput(BaseModel):
|
|
"""
|
|
A single client-submitted event/input item for a session.
|
|
|
|
Used both as an element of ``initial_items`` on session
|
|
creation and as the body of ``POST /v1/sessions/{id}/events``.
|
|
Carries a discriminator (``type``) and a free-form ``data``
|
|
payload whose shape is interpreted by the route layer based
|
|
on ``type`` (e.g. user message, function-call output,
|
|
approval, interrupt).
|
|
|
|
:param model_override: Optional per-event LLM model override
|
|
used when this event starts a fresh agent turn. Distinct
|
|
from the session's bound agent; it substitutes for the
|
|
agent spec's ``llm.model`` for that turn.
|
|
:param type: Discriminator for the event/input kind, e.g.
|
|
``"message"``, ``"function_call_output"``, ``"interrupt"``.
|
|
:param data: Type-specific payload. Shape varies by ``type``;
|
|
for ``"message"`` this looks like
|
|
``{"role": "user", "content": [{"type": "input_text",
|
|
"text": "Hello"}]}``. For ``"interrupt"`` this is
|
|
typically ``{}``.
|
|
:param tools: Optional OpenAI function-tool dicts registered
|
|
when this event creates a new task. Mirrors
|
|
:attr:`CreateResponseRequest.tools`, e.g. ``[{"type":
|
|
"function", "function": {"name": "get_weather",
|
|
"description": "...", "parameters": {...}}}]``. Ignored
|
|
when the event steers into an active task: that task's
|
|
tools are fixed at start time.
|
|
"""
|
|
|
|
type: str
|
|
# Heterogeneous payload; route layer validates the shape per ``type``.
|
|
# Defaults to {} for payload-less control events (interrupt,
|
|
# stop_session); item-typed events still fail loud per-type.
|
|
data: dict[str, Any] = Field(default_factory=dict)
|
|
model_override: str | None = None
|
|
tools: list[dict[str, Any]] | None = None
|
|
|
|
|
|
class SessionGitOptions(BaseModel):
|
|
"""
|
|
Git worktree options for ``POST /v1/sessions``.
|
|
|
|
Requires ``host_id`` to be set (and therefore ``workspace``, which
|
|
is interpreted as the source repository directory). Two modes,
|
|
selected by ``existing_worktree``:
|
|
|
|
- **create** (default): the server creates a git worktree on the
|
|
host for a new branch and starts the runner in that worktree
|
|
instead of the picked directory.
|
|
- **bind** (``existing_worktree=True``): ``workspace`` already IS a
|
|
pre-existing worktree; no worktree is created. ``branch_name`` is
|
|
recorded as the session's ``git_branch`` for display and opt-in
|
|
cleanup, and ``base_branch`` must not be set.
|
|
|
|
See designs/SESSION_GIT_WORKTREE.md.
|
|
|
|
:param branch_name: In create mode, the new branch to create and
|
|
check out, e.g. ``"feature/login"``. In bind mode, the branch
|
|
already checked out in the existing worktree. Validated against
|
|
git ref-format rules; invalid names fail with ``invalid_input``.
|
|
:param base_branch: Optional base ref to branch from, e.g.
|
|
``"main"`` or ``"origin/main"``. ``None`` branches from the
|
|
source repository's current ``HEAD``. Create mode only —
|
|
invalid with ``existing_worktree``.
|
|
:param existing_worktree: When ``True``, bind to the pre-existing
|
|
worktree at ``workspace`` instead of creating one (see above).
|
|
"""
|
|
|
|
branch_name: str
|
|
base_branch: str | None = None
|
|
existing_worktree: bool = False
|
|
|
|
@model_validator(mode="after")
|
|
def _check_existing_worktree(self) -> SessionGitOptions:
|
|
"""Reject ``base_branch`` in bind mode (422).
|
|
|
|
``base_branch`` selects the ref a *new* branch forks from; it is
|
|
meaningless when binding to a worktree that already exists.
|
|
|
|
:returns: The validated instance.
|
|
:raises ValueError: If ``base_branch`` is set with
|
|
``existing_worktree``.
|
|
"""
|
|
if self.existing_worktree and self.base_branch is not None:
|
|
raise ValueError("base_branch cannot be set when existing_worktree is true")
|
|
return self
|
|
|
|
|
|
class SessionCreateRequest(BaseModel):
|
|
"""
|
|
JSON request body for ``POST /v1/sessions``.
|
|
|
|
Creates a new session bound to an existing agent (looked up by
|
|
durable agent ID, NOT name) and optionally seeds its input queue.
|
|
|
|
The Alpha runner-state bundled create flow adds a multipart shape
|
|
to the same endpoint; this JSON body remains the existing
|
|
session-create contract for clients that already uploaded an agent.
|
|
|
|
:param agent_id: Durable identifier of the agent to bind,
|
|
e.g. ``"ag_abc123"``. Must match a registered agent.
|
|
:param initial_items: Initial queued events/inputs, typically a
|
|
single user ``"message"``.
|
|
:param title: Optional human-readable title for the session,
|
|
e.g. ``"debugging auth flow"``.
|
|
:param labels: Initial guardrails labels to set on the session.
|
|
:param parent_session_id: Parent session for sub-agent spawns.
|
|
When set, the server inherits the parent's ``runner_id``
|
|
affinity and sets ``parent_conversation_id`` on the child
|
|
conversation. ``None`` for top-level sessions.
|
|
:param sub_agent_name: For sub-agent sessions, the sub-agent
|
|
type name within the parent's spec tree, e.g.
|
|
``"summarizer"``. The runner uses this to load the correct
|
|
sub-spec instead of the parent's. ``None`` for top-level.
|
|
:param host_type: How the session's host is obtained.
|
|
``"external"`` (the default, and the pre-existing behavior):
|
|
the session runs on a host the caller manages — either a
|
|
host they 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 and binds the session to it —
|
|
``host_id`` and ``workspace`` must NOT be set (the server
|
|
chooses both). Provisioning happens in the BACKGROUND: the
|
|
create returns immediately with ``host_id`` / ``workspace``
|
|
still null, and they appear on the session snapshot once
|
|
the sandbox host registers. A message posted before then
|
|
waits for the launch to settle instead of failing with
|
|
"no runner bound".
|
|
:param host_id: Optional host to launch the runner on, e.g.
|
|
``"host_a1b2c3d4..."``. When set, the server triggers the
|
|
host launch flow (generate binding token, write runner_id,
|
|
send launch frame). ``None`` for CLI-initiated sessions.
|
|
Must be ``None`` when ``host_type`` is ``"managed"``.
|
|
:param workspace: Where the session works. For external hosts:
|
|
an absolute path on the host where the runner should start,
|
|
e.g. ``"/Users/corey/universe/src/foo"``. Required when
|
|
``host_id`` is set; the server validates that the path
|
|
exists, falls within the agent's ``os_env.cwd`` boundary,
|
|
and contains any subdirectory the agent expects (per
|
|
designs/SESSION_WORKSPACE_SELECTION.md). Tilde paths
|
|
(``~/foo``) and relative paths are rejected — the server
|
|
does not expand ``~``. Optional for CLI-initiated sessions
|
|
that record their starting cwd for display. For
|
|
``host_type: "managed"``: optionally a git repository URL
|
|
with a ``#<branch>`` fragment, e.g.
|
|
``"https://github.com/org/repo#main"`` or
|
|
``"git@github.com:org/repo.git"`` — the server clones it
|
|
inside the sandbox and the cloned directory becomes the
|
|
stored session workspace (paths are rejected; ``None``
|
|
gives an empty server-created workspace).
|
|
:param git: Optional git worktree options. When set, the server
|
|
creates a worktree for a new branch on the host and starts
|
|
the runner in it; ``workspace`` is then interpreted as the
|
|
source repository directory. Requires ``host_id``. ``None``
|
|
starts the runner directly in ``workspace``. See
|
|
designs/SESSION_GIT_WORKTREE.md.
|
|
:param terminal_launch_args: 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.
|
|
Bounds (count / length) are validated server-side. ``None`` for
|
|
non-native sessions. Mirrors the multipart create path
|
|
(:class:`SessionCreateMetadata`). See
|
|
designs/NATIVE_RUNNER_SERVER_LAUNCH.md.
|
|
:param model_override: Optional per-session LLM model override to
|
|
persist at create time, e.g. ``"databricks-claude-sonnet-4-6"``.
|
|
Set by ``sys_session_send``'s per-dispatch ``model`` arg so the
|
|
value is on the session row before the runner launches the
|
|
harness (native CLIs read it as ``--model`` at terminal launch;
|
|
SDK harnesses via the spawn env). Validated server-side against
|
|
a conservative model-id charset. ``None`` = harness default.
|
|
:param reasoning_effort: Optional per-session reasoning-effort
|
|
override to persist at create time, e.g. ``"high"``. Set by the
|
|
web UI's new-chat model/effort picker (claude-native today) so
|
|
the value is on the session row before the runner launches the
|
|
harness — native Claude Code reads it as ``--effort`` at terminal
|
|
launch; SDK harnesses via the spawn env. Validated server-side
|
|
against the shared effort vocabulary; provider-specific support
|
|
is enforced downstream at launch. ``None`` = harness default.
|
|
Mirrors the multipart create path (:class:`SessionCreateMetadata`).
|
|
:param cost_control_mode_override: Optional per-session
|
|
cost-control switch to persist at create time: ``"on"``
|
|
activates the spec's configured cost-control mode, ``"off"``
|
|
disables cost control for this session. ``None`` (the
|
|
default) defers to the spec default. Set by the web UI's
|
|
new-session "Cost Optimized" option; read by the cost-control
|
|
advisor pipeline at turn start.
|
|
:param harness_override: Optional per-session brain-harness
|
|
override to persist at create time, e.g. ``"pi"`` or
|
|
``"openai-agents"``. Set by the web UI's new-chat harness
|
|
picker; the runner uses it instead of the agent spec's
|
|
``executor.config.harness`` when spawning the harness for
|
|
this session. Validated server-side: must canonicalize into
|
|
``OMNIGENT_HARNESSES`` and the bound agent must be an
|
|
``executor.type: omnigent`` spec. ``None`` (the default) uses
|
|
the spec's declared harness. Create-time only — there is no
|
|
PATCH path, since the harness process spawns on the first
|
|
turn.
|
|
"""
|
|
|
|
agent_id: str
|
|
initial_items: list[SessionEventInput] = Field(default_factory=list)
|
|
title: str | None = None
|
|
labels: dict[str, str] = Field(default_factory=dict)
|
|
parent_session_id: str | None = None
|
|
sub_agent_name: str | None = None
|
|
host_type: Literal["external", "managed"] = "external"
|
|
host_id: str | None = None
|
|
workspace: str | None = None
|
|
git: SessionGitOptions | None = None
|
|
terminal_launch_args: list[str] | None = None
|
|
model_override: str | None = None
|
|
reasoning_effort: str | None = None
|
|
cost_control_mode_override: str | None = None
|
|
harness_override: str | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def _check_git_requires_host(self) -> SessionCreateRequest:
|
|
"""
|
|
Reject ``git`` without ``host_id`` at validation time.
|
|
|
|
Worktree creation runs on a host (the server has no
|
|
filesystem), so ``git`` is meaningless without ``host_id``.
|
|
Failing here returns 422 instead of letting the request reach
|
|
the worktree path and fail late.
|
|
|
|
:returns: The validated instance.
|
|
:raises ValueError: If ``git`` is set but ``host_id`` is not.
|
|
"""
|
|
if self.git is not None and self.host_id is None:
|
|
raise ValueError("git worktree creation requires host_id")
|
|
return self
|
|
|
|
@model_validator(mode="after")
|
|
def _check_managed_host_fields(self) -> SessionCreateRequest:
|
|
"""
|
|
Enforce the per-``host_type`` workspace and host-id contract.
|
|
|
|
A managed session's host is chosen by the server (sandbox
|
|
provisioning), so a caller-supplied ``host_id`` is a
|
|
contradiction. Its ``workspace``, when given, must be a git
|
|
repository URL (optionally ``#<branch>``) the server clones
|
|
into the sandbox — a path points at nothing in a sandbox that
|
|
doesn't exist yet. Conversely, a repository-URL workspace on
|
|
an external host is rejected: there, ``workspace`` is an
|
|
absolute path on the host. Failing at validation returns a
|
|
422 with the field named instead of silently ignoring the
|
|
caller's intent.
|
|
|
|
:returns: The validated instance.
|
|
:raises ValueError: On ``"managed"`` + ``host_id``, a managed
|
|
workspace that isn't a valid repository URL, or an
|
|
external repository-URL workspace.
|
|
"""
|
|
# Lazy import: schemas is imported by nearly every module, so
|
|
# pulling the (FastAPI/click-importing) managed-hosts module in
|
|
# at module scope would risk import cycles.
|
|
from omnigent.server.managed_hosts import is_repo_workspace, parse_repo_workspace
|
|
|
|
if self.host_type == "managed":
|
|
if self.host_id is not None:
|
|
raise ValueError(
|
|
"host_type 'managed' lets the server provision the host; "
|
|
"host_id must not be set"
|
|
)
|
|
if self.workspace is not None:
|
|
try:
|
|
parse_repo_workspace(self.workspace)
|
|
except ValueError as exc:
|
|
raise ValueError(
|
|
"host_type 'managed' takes a git repository URL "
|
|
f"(optionally '#<branch>') as workspace: {exc}"
|
|
) from exc
|
|
elif self.workspace is not None and is_repo_workspace(self.workspace):
|
|
raise ValueError(
|
|
"a repository-URL workspace requires host_type 'managed' — "
|
|
"external hosts take an absolute path on the host"
|
|
)
|
|
return self
|
|
|
|
|
|
class SessionCreateMetadata(BaseModel):
|
|
"""
|
|
Metadata JSON part for multipart ``POST /v1/sessions``.
|
|
|
|
The uploaded agent tarball supplies the agent spec. This JSON
|
|
part carries only session-level metadata so request metadata
|
|
cannot disagree with the agent bundle.
|
|
|
|
:param title: Optional human-readable title for the session,
|
|
e.g. ``"debugging auth flow"``.
|
|
:param labels: Initial guardrails labels to set on the
|
|
session. Empty dict (the default) starts with no labels.
|
|
:param reasoning_effort: 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. ``None`` means use the agent
|
|
default.
|
|
:param host_id: Optional host to launch the runner on, e.g.
|
|
``"host_a1b2c3d4..."``. When set, the server generates a
|
|
binding token, writes the expected runner_id to the session
|
|
row, and sends a ``host.launch_runner`` frame to the host.
|
|
``None`` for CLI-initiated sessions where the caller
|
|
manages runner spawning.
|
|
:param workspace: Absolute path on the host where the runner
|
|
should start, e.g. ``"/Users/corey/universe/src/foo"``.
|
|
Required when ``host_id`` is set; validated against the
|
|
uploaded agent's ``os_env.cwd`` boundary at session create
|
|
(per designs/SESSION_WORKSPACE_SELECTION.md). Optional
|
|
otherwise.
|
|
:param terminal_launch_args: Optional pass-through CLI args for a
|
|
native terminal wrapper (claude / codex), e.g.
|
|
``["--dangerously-skip-permissions"]``. Set at create-time so
|
|
the runner has them before it boots. Bounds (count / length)
|
|
are validated server-side. ``None`` for non-native sessions.
|
|
See designs/NATIVE_RUNNER_SERVER_LAUNCH.md.
|
|
:param parent_session_id: Optional parent session id, e.g.
|
|
``"conv_abc123"``. When set, the new session is created as a
|
|
sub-agent child of that session (``kind="sub_agent"``) and
|
|
inherits the parent's runner binding for co-location. The
|
|
caller must have READ access to the parent. ``None``
|
|
creates a top-level session.
|
|
"""
|
|
|
|
title: str | None = None
|
|
labels: dict[str, str] = Field(default_factory=dict)
|
|
reasoning_effort: str | None = None
|
|
host_id: str | None = None
|
|
workspace: str | None = None
|
|
terminal_launch_args: list[str] | None = None
|
|
parent_session_id: str | None = None
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class CreatedSessionResponse(BaseModel):
|
|
"""
|
|
Response body for multipart ``POST /v1/sessions``.
|
|
|
|
:param session_id: Identifier of the newly created session,
|
|
e.g. ``"conv_abc123"``.
|
|
:param agent_id: Identifier of the session-scoped agent created
|
|
from the uploaded bundle, e.g. ``"ag_abc123"``.
|
|
:param agent_name: Agent name loaded from the uploaded bundle's
|
|
spec, e.g. ``"code-assistant"``.
|
|
"""
|
|
|
|
session_id: str
|
|
agent_id: str
|
|
agent_name: str
|
|
|
|
|
|
class SessionLabelsResponse(BaseModel):
|
|
"""
|
|
Lightweight response body for ``GET /v1/sessions/{id}/labels``.
|
|
|
|
:param id: Session identifier, e.g. ``"conv_abc123"``.
|
|
:param labels: Session-scoped guardrails labels. Empty dict when
|
|
no labels have been written.
|
|
"""
|
|
|
|
id: str
|
|
labels: dict[str, str] = Field(default_factory=dict)
|
|
|
|
|
|
# Stages of a managed-sandbox launch, in pipeline order: the sandbox
|
|
# is provisioned, the repository workspace is cloned into it (skipped
|
|
# when the session has no repo workspace), the in-sandbox host starts
|
|
# and registers, and the agent runner is launched on it. ``ready`` and
|
|
# ``failed`` are terminal.
|
|
SandboxLaunchStage = Literal[
|
|
"provisioning",
|
|
"cloning",
|
|
"starting",
|
|
"connecting",
|
|
"ready",
|
|
"failed",
|
|
]
|
|
|
|
|
|
class SandboxStatus(BaseModel):
|
|
"""
|
|
Managed-sandbox launch progress for a ``host_type="managed"`` session.
|
|
|
|
Carried on the session snapshot only while the session's
|
|
background sandbox launch is in flight or has failed; ``None``
|
|
for sessions without a managed launch and once the launch
|
|
succeeds (the session then looks like any host-bound session).
|
|
|
|
:param stage: Current launch stage, e.g. ``"provisioning"`` —
|
|
one of :data:`SandboxLaunchStage`, in pipeline order:
|
|
``provisioning`` (creating the sandbox) → ``cloning``
|
|
(cloning the repository workspace; skipped when the session
|
|
has none) → ``starting`` (starting the in-sandbox host) →
|
|
``connecting`` (launching the agent runner) → ``ready`` /
|
|
``failed``.
|
|
:param error: Failure detail when ``stage == "failed"``, e.g.
|
|
``"managed sandbox launch failed: spend limit reached"``.
|
|
``None`` otherwise.
|
|
"""
|
|
|
|
stage: SandboxLaunchStage
|
|
error: str | None = None
|
|
|
|
|
|
class ModelUsage(BaseModel):
|
|
"""
|
|
Cumulative token/cost usage attributed to a single LLM model.
|
|
|
|
One value in the ``usage_by_model`` map on :class:`SessionResponse` /
|
|
:class:`SessionUsageEvent`, keyed by the raw harness-reported model id
|
|
(e.g. ``"claude-sonnet-4-6"``, ``"databricks-gpt-5-5"``). Counts are
|
|
summed over the session's subtree (itself + sub-agent descendants), so a
|
|
parent folds in sub-agents that ran a different model. Token buckets
|
|
mirror the flat per-session breakdown.
|
|
|
|
:param input_tokens: Cumulative non-cached input (prompt) tokens for this
|
|
model over the subtree, e.g. ``12000``. ``None`` when not recorded.
|
|
:param output_tokens: Cumulative output (completion) tokens, e.g.
|
|
``3400``. ``None`` when not recorded.
|
|
:param total_tokens: Cumulative total tokens (counts cache buckets too,
|
|
as the harness reports), e.g. ``15400``. ``None`` when not recorded.
|
|
:param cache_read_input_tokens: Cumulative tokens read from the prompt
|
|
cache, e.g. ``8000``. ``None`` when not recorded.
|
|
:param cache_creation_input_tokens: Cumulative tokens written to the
|
|
prompt cache, e.g. ``2000``. ``None`` when not recorded.
|
|
:param total_cost_usd: Cumulative USD spend attributed to this model,
|
|
e.g. ``0.42``. Present **only when this model's turns were priced**
|
|
(same "priced ⟺ key present" contract as the session total); ``None``
|
|
when the model is unpriced, so the sum of priced per-model costs
|
|
equals the session ``total_cost_usd``.
|
|
"""
|
|
|
|
input_tokens: int | None = None
|
|
output_tokens: int | None = None
|
|
total_tokens: int | None = None
|
|
cache_read_input_tokens: int | None = None
|
|
cache_creation_input_tokens: int | None = None
|
|
total_cost_usd: float | None = None
|
|
|
|
|
|
class SessionResponse(BaseModel):
|
|
"""
|
|
API representation of a session.
|
|
|
|
Returned by ``POST /v1/sessions``, ``GET /v1/sessions/{id}``,
|
|
and ``PATCH /v1/sessions/{id}``.
|
|
|
|
:param id: Unique session identifier (also the underlying
|
|
conversation ID), e.g. ``"conv_abc123"``.
|
|
:param agent_id: Durable identifier of the bound agent,
|
|
e.g. ``"ag_abc123"``. Stable across renames of the
|
|
agent.
|
|
:param agent_name: Human-readable name of the bound agent,
|
|
e.g. ``"research-agent"``. Loaded from the agent row at
|
|
snapshot-build time. ``None`` when the agent row cannot
|
|
be found (deleted or orphaned session).
|
|
:param status: Session lifecycle status. One of
|
|
``"idle"`` (no loop running), ``"running"`` (loop
|
|
executing), ``"waiting"`` (loop parked on background
|
|
work / sub-agents), or ``"failed"`` (terminal failure).
|
|
Current read paths collapse ``"waiting"`` -> ``"running"``
|
|
before building this snapshot; the literal stays a superset
|
|
of what the runtime can produce so a server that forwards
|
|
the raw status never 500s on serialization.
|
|
:param background_task_count: Background shells (claude-native) still
|
|
running as of the last status edge, so a reload re-shows "N shells
|
|
still running" even though the session has settled to ``"idle"``.
|
|
``None`` (the default / omitted) when no shells are tracked.
|
|
:param created_at: Unix epoch seconds of creation.
|
|
:param title: Optional human-readable title, e.g.
|
|
``"debugging auth flow"``. ``None`` when unset.
|
|
:param labels: Session-scoped guardrails labels. Empty dict
|
|
when no labels have been written.
|
|
:param runner_id: Runner currently bound to this session, e.g.
|
|
``"runner_abc123"``. ``None`` until a client binds one via
|
|
``PATCH /v1/sessions/{id}``.
|
|
:param host_id: Host that launched (or should launch) the
|
|
runner for this session, e.g. ``"host_a1b2c3d4..."``.
|
|
``None`` for CLI-initiated sessions.
|
|
:param runner_online: Strict runner liveness — ``True`` iff a
|
|
runner tunnel is currently registered for this session.
|
|
This is the sole reachability signal: ``True`` means the
|
|
client can chat normally. It does **not** fold in
|
|
host-relaunch optimism (a dead runner on a live host reads
|
|
``False`` here, not ``True``) — the open-session view pairs
|
|
it with ``host_online`` to decide what to show. ``None``
|
|
when the server has no runner liveness lookup wired.
|
|
:param host_online: Whether the session's host tunnel is live
|
|
(status online and fresh within the host liveness TTL).
|
|
``None`` when the session has no ``host_id`` (CLI/local).
|
|
Used only to choose what the open view shows when
|
|
``runner_online`` is ``False`` — host alive ⇒ "send a
|
|
message to wake the runner"; host dead ⇒ "reconnect /
|
|
fork". Never participates in the reachability decision.
|
|
:param host_resumable: Whether this session is bound to a dormant
|
|
managed host the server can wake in place (its provider sets
|
|
:attr:`SandboxLauncher.can_resume`). The open view reads it only
|
|
when ``host_online`` is ``False``, to split a confirmed host-down
|
|
into a recoverable "asleep" state (send a message — the relaunch
|
|
path resumes the sandbox) versus the terminal ``host_offline``
|
|
dead-end (reconnect from your machine / fork). ``False`` for
|
|
non-managed or non-resumable hosts.
|
|
:param reasoning_effort: 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. ``None`` means use the agent default.
|
|
:param items: Committed conversation items in chronological
|
|
order. Empty for a freshly created session.
|
|
:param sub_agent_name: For sub-agent sessions, the sub-agent
|
|
type name within the parent's spec tree, e.g.
|
|
``"summarizer"``. ``None`` for top-level sessions.
|
|
:param parent_session_id: For sub-agent sessions, the parent
|
|
conversation's id, e.g. ``"conv_parent987"``. ``None`` for
|
|
top-level sessions. Lets clients identify a session as a
|
|
child and link back to its parent without an extra
|
|
round-trip — the same conversation row exposes this via
|
|
``parent_conversation_id`` internally.
|
|
:param root_conversation_id: The id of this session's spawn-tree
|
|
root, e.g. ``"conv_root1"``. Equals ``id`` for top-level
|
|
sessions; for sub-agents it points at the top-level ancestor.
|
|
Lets orchestration tools (e.g. ``sys_session_close``) confirm
|
|
a target shares the caller's spawn tree over the REST path.
|
|
``None`` only when the underlying row predates the
|
|
``root_conversation_id`` column (not expected post-migration).
|
|
:param permission_level: The requesting user's numeric
|
|
permission level on this session: ``1`` = read, ``2`` =
|
|
edit, ``3`` = manage. ``None`` when permissions are
|
|
disabled (single-user mode without a permission store).
|
|
:param llm_model: The LLM model identifier from the bound
|
|
agent's spec, e.g. ``"anthropic/claude-sonnet-4-6"``.
|
|
``None`` when the agent has no explicit ``llm:`` block or
|
|
the agent cannot be looked up.
|
|
:param harness: The bound agent's canonical harness, e.g.
|
|
``"claude-sdk"`` or ``"openai-agents"``. Lets the client
|
|
render the active credential for the correct provider
|
|
family instead of inferring it from the model string (which
|
|
is wrong when the agent declares no model). ``None`` when
|
|
the agent cannot be looked up.
|
|
:param model_override: Per-session LLM model override,
|
|
e.g. ``"claude-opus-4-7"``. ``None`` means no override is
|
|
active (the agent's ``llm_model`` applies). Set via
|
|
``PATCH /v1/sessions/{id}`` or the REPL's ``/model``
|
|
command; both write the same column so the web UI and
|
|
the TUI stay in sync.
|
|
:param cost_control_mode_override: Per-session cost-control
|
|
switch: ``"on"`` activates the spec's configured cost-control
|
|
mode, ``"off"`` disables cost control for this session.
|
|
``None`` means no override is active (the spec default
|
|
applies). Set at create time or via
|
|
``PATCH /v1/sessions/{id}`` (the web "Cost Optimized"
|
|
toggle); read by the cost-control advisor pipeline.
|
|
:param context_window: The model's context window size in tokens
|
|
as looked up server-side from litellm's registry (or from the
|
|
``AP_CONTEXT_WINDOW_OVERRIDE`` env var), e.g. ``200_000``.
|
|
``None`` when the model is not in litellm's registry and no
|
|
override is set.
|
|
:param last_total_tokens: Total token count (input + output) from
|
|
the most recently completed task's ``usage``, e.g. ``45231``.
|
|
``None`` when no task has completed yet. Lets clients seed
|
|
their context-ring on conversation resume without waiting for
|
|
the next ``response.completed`` SSE event.
|
|
:param total_cost_usd: Cumulative LLM spend for this session in
|
|
USD, e.g. ``0.42``. ``None`` when the session is **unpriced**
|
|
— no turn has been priced yet (the model is absent from the
|
|
pricing catalog, or no usage has been recorded) — so clients
|
|
render "—" rather than a misleading ``$0.00``. Server-computed
|
|
(cache-aware for relay/codex, exact billing for claude-native),
|
|
the same total the cost-budget policy gates on. Lets clients
|
|
seed their cost indicator on resume without waiting for the
|
|
next ``session.usage`` SSE event.
|
|
:param usage_by_model: Per-model breakdown of the same subtree usage,
|
|
keyed by the raw harness model id, e.g.
|
|
``{"claude-sonnet-4-6": ModelUsage(input_tokens=12000, ...)}``.
|
|
``None`` when no per-model usage has been recorded (older sessions
|
|
recorded before this field existed, or before the first turn). Lets
|
|
the UI show which models a session spent its tokens / budget on.
|
|
:param last_task_error: Error details from the most recently
|
|
failed task. Only present when ``status == "failed"`` and
|
|
the task stored an error. Lets clients display the failure
|
|
reason on historical load without relying on the transient
|
|
``response.error`` SSE event (which may have been emitted
|
|
before the web client subscribed). Format mirrors the
|
|
``RetryErrorDetail`` SSE shape:
|
|
``{"code": "executor_error", "message": "..."}``.
|
|
``None`` in all other cases.
|
|
:param external_session_id: Runtime-native session id this
|
|
conversation wraps, e.g. a Claude Code session uuid for
|
|
``omnigent claude`` sessions. ``None`` for regular
|
|
AP-only conversations. Populated by the wrapper bridge.
|
|
:param terminal_launch_args: Pass-through CLI args the native
|
|
terminal wrapper (claude / codex) was launched with, e.g.
|
|
``["--dangerously-skip-permissions"]``. ``None`` for
|
|
non-native sessions or a native session launched with none.
|
|
Lets the launcher reproduce the command on resume.
|
|
:param pending_elicitations: Outstanding approval prompts on
|
|
this session at the moment the snapshot was built — the
|
|
original ``response.elicitation_request`` event dicts.
|
|
Lets the UI render the ApprovalCard on cold load, since
|
|
the live SSE stream has no replay and a prompt emitted
|
|
before the user opened the chat would otherwise vanish.
|
|
Empty list when no prompts are outstanding. Sourced from
|
|
the Omnigent server's in-memory
|
|
:mod:`omnigent.runtime.pending_elicitations` index.
|
|
:param pending_inputs: Un-consumed web-composer user messages on
|
|
native-terminal (claude-native / codex-native) sessions at
|
|
snapshot time, each ``{"pending_id", "content"}``. Native
|
|
sessions don't persist a web message at POST time (the
|
|
transcript forwarder is the single writer), so a client that
|
|
posted then navigated away / rebound would lose its optimistic
|
|
bubble; replaying these re-hydrates it. Empty list otherwise.
|
|
Sourced from the in-memory
|
|
:mod:`omnigent.runtime.pending_inputs` index.
|
|
:param workspace: Absolute path on disk where the runner cd's,
|
|
e.g. ``"/Users/corey/universe/src/foo"``. Set when the
|
|
session was bound to a host workspace at create-time, or
|
|
when the CLI captured ``os.getcwd()`` at session-create.
|
|
Always ``None`` when not yet validated against a host. When a
|
|
git worktree was created for the session, this is the
|
|
worktree directory path.
|
|
:param git_branch: 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;
|
|
``None`` otherwise. The Web UI uses a non-``None`` value to
|
|
offer the "delete local branch" cleanup checkbox on session
|
|
delete. See designs/SESSION_GIT_WORKTREE.md.
|
|
:param archived: Whether the session is archived. Archived
|
|
sessions are hidden from the default sidebar listing and
|
|
surface only behind the "Show archived" toggle. ``False``
|
|
for normal sessions. Toggled via ``PATCH /v1/sessions/{id}``.
|
|
:param todos: Current Claude Code todo list items for
|
|
``omnigent claude`` sessions, as raw dicts from Claude's
|
|
todo JSON file. Each dict has ``content``, ``status``,
|
|
and ``activeForm`` keys. Empty list for non-claude-native
|
|
sessions or when no todos have been reported yet. Sourced
|
|
from the Omnigent server's in-memory ``_session_todos_cache``.
|
|
:param skills: Skills the bound agent has access to — the
|
|
merged result of the agent spec's bundled ``skills``
|
|
and the host-scope skills discovered along the agent
|
|
workdir / ``~/.claude/skills/`` (subject to the spec's
|
|
``skills_filter``). Mirrors what the TUI passes to the
|
|
runner at startup. Empty list when the agent spec
|
|
cannot be loaded, or when bundled + host discovery
|
|
yields nothing.
|
|
:param model_options: Codex app-server ``model/list`` options
|
|
for codex-native sessions, including each model's supported
|
|
reasoning efforts. Empty for non-codex-native sessions or while
|
|
the bound runner / Codex app-server cannot answer yet.
|
|
:param terminal_pending: ``True`` while the runner is auto-creating
|
|
a terminal-first session's terminal (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. Sourced from the Omnigent server's in-memory
|
|
``_session_terminal_pending_cache`` at snapshot build time, so a
|
|
client connecting mid-spin-up still sees the spinner.
|
|
:param sandbox_status: Managed-sandbox launch progress while the
|
|
session's background sandbox launch is in flight or has
|
|
failed — see :class:`SandboxStatus`. ``None`` for sessions
|
|
without a managed launch and once the launch succeeds.
|
|
Sourced from the Omnigent server's in-memory
|
|
``_session_sandbox_status_cache`` at snapshot build time, so
|
|
a client opening the session mid-launch sees the current
|
|
stage.
|
|
:param active_response_id: Response id of the turn currently in
|
|
flight, or ``None`` when the session is idle. Sourced from the
|
|
server's ``_session_active_response_cache`` at snapshot build
|
|
time so a client connecting mid-turn can reopen a streaming
|
|
``activeResponse`` — the SSE stream is snapshot + live tail with
|
|
no replay, so the turn-start ``running`` edge that carried this
|
|
id is not re-sent on reconnect. Today only native-terminal
|
|
forwarders (claude-native) stamp a turn id on their status
|
|
edges; other harnesses leave this ``None``.
|
|
"""
|
|
|
|
id: str
|
|
agent_id: str
|
|
agent_name: str | None = None
|
|
status: Literal["idle", "running", "waiting", "failed"]
|
|
background_task_count: int | None = None
|
|
created_at: int
|
|
title: str | None = None
|
|
labels: dict[str, str] = Field(default_factory=dict)
|
|
runner_id: str | None = None
|
|
host_id: str | None = None
|
|
runner_online: bool | None = None
|
|
host_online: bool | None = None
|
|
host_resumable: bool = False
|
|
reasoning_effort: str | None = None
|
|
items: list[ConversationItem] = Field(default_factory=list)
|
|
permission_level: int | None = None
|
|
sub_agent_name: str | None = None
|
|
parent_session_id: str | None = None
|
|
root_conversation_id: str | None = None
|
|
llm_model: str | None = None
|
|
harness: str | None = None
|
|
model_override: str | None = None
|
|
cost_control_mode_override: str | None = None
|
|
context_window: int | None = None
|
|
last_total_tokens: int | None = None
|
|
total_cost_usd: float | None = None
|
|
usage_by_model: dict[str, ModelUsage] | None = None
|
|
last_task_error: dict[str, str] | None = None
|
|
external_session_id: str | None = None
|
|
terminal_launch_args: list[str] | None = None
|
|
pending_elicitations: list[dict[str, Any]] = Field(default_factory=list)
|
|
# Un-consumed web-composer user messages on native-terminal
|
|
# sessions at snapshot time, each ``{"pending_id", "content"}``.
|
|
# Replayed so a client that posted a message then navigated away /
|
|
# rebound re-hydrates the optimistic bubble (the live SSE stream
|
|
# has no replay). Empty for non-native sessions, which persist the
|
|
# message at POST time and thus already carry it in ``items``.
|
|
# Source: :mod:`omnigent.runtime.pending_inputs`.
|
|
pending_inputs: list[dict[str, Any]] = Field(default_factory=list)
|
|
workspace: str | None = None
|
|
git_branch: str | None = None
|
|
archived: bool = False
|
|
todos: list[dict[str, Any]] = Field(default_factory=list)
|
|
skills: list[SkillSummary] = Field(default_factory=list)
|
|
model_options: list[dict[str, Any]] = Field(default_factory=list)
|
|
terminal_pending: bool = False
|
|
sandbox_status: SandboxStatus | None = None
|
|
# Per-MCP-server startup state for native harness sessions
|
|
# (codex-native), present while the harness boots its MCP servers or
|
|
# when servers were cancelled/failed. ``None`` otherwise. Sourced from
|
|
# ``_session_mcp_startup_cache`` at snapshot build time so a client
|
|
# opening the session mid-startup sees the startup band.
|
|
mcp_startup: dict[str, McpServerStartup] | None = None
|
|
active_response_id: str | None = None
|
|
|
|
|
|
class UpdateSessionRequest(BaseModel):
|
|
"""
|
|
Request body for ``PATCH /v1/sessions/{id}``.
|
|
|
|
The Alpha runner-state pivot makes this endpoint the mutable
|
|
session affinity primitive when ``runner_id`` is provided. The
|
|
server validates that the runner is online, then replaces
|
|
``conversations.runner_id``. Existing session metadata updates
|
|
remain supported for clients that update title, labels, or
|
|
reasoning effort through the sessions API.
|
|
|
|
:param runner_id: Identifier of a registered runner, e.g.
|
|
``"runner_abc123"``. ``None`` leaves runner binding
|
|
unchanged.
|
|
:param title: New title, e.g. ``"debugging auth flow"``.
|
|
``None`` leaves unchanged.
|
|
:param labels: Guardrails labels to upsert. Merges with existing
|
|
labels; keys not present are left untouched.
|
|
:param reasoning_effort: 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 aliases such as ``"default"`` remove
|
|
the session override. ``None`` leaves unchanged.
|
|
:param model_override: Per-session LLM model override, e.g.
|
|
``"claude-opus-4-7"``. The value is forwarded as-is to the
|
|
executor at turn start; the server does not enumerate valid
|
|
models. Clear aliases such as ``"default"``, ``"off"``, or
|
|
``"reset"`` remove the override (matching the REPL's
|
|
``/model`` semantics). ``None`` leaves unchanged.
|
|
:param collaboration_mode: Codex-native collaboration-mode string.
|
|
``"plan"`` enters Plan mode and ``"default"`` returns to Default
|
|
mode for subsequent Codex turns. Only valid for sessions stamped
|
|
with the codex-native wrapper label. Omitted leaves unchanged.
|
|
:param cost_control_mode_override: Per-session cost-control
|
|
switch: ``"on"`` activates the spec's configured cost-control
|
|
mode, ``"off"`` disables cost control for this session.
|
|
Explicit JSON ``null`` clears the override back to the spec
|
|
default; omitting the field leaves it unchanged (``"off"`` is
|
|
a real value here, so the field's *presence* — not a clear
|
|
alias — is the clear signal, unlike ``model_override``).
|
|
:param external_session_id: Runtime-native session id captured
|
|
by a wrapper bridge (e.g. Claude Code's session uuid for
|
|
``omnigent claude`` sessions). Idempotent on same-value
|
|
writes; the server rejects attempts to overwrite an
|
|
already-set different value with ``invalid_input`` to
|
|
surface programmer errors. ``None`` leaves unchanged.
|
|
:param terminal_launch_args: Per-session native-terminal
|
|
pass-through args, e.g. ``["--dangerously-skip-permissions"]``.
|
|
A list (including ``[]``) replaces the stored value wholesale
|
|
— resume is last-write-wins, never an append. Bounds (count /
|
|
length) are validated server-side. ``None`` leaves unchanged.
|
|
:param silent: When ``True``, persist metadata changes but skip
|
|
the runner-side side effects — specifically the
|
|
native ``/effort`` / ``/model`` / Codex collaboration-mode
|
|
forwards into the live runtime. Used by automatic bind-time
|
|
handoffs (web's sticky-pref apply on session switch, the
|
|
REPL's pre-create ``/model`` snapshot) where injecting a
|
|
visible slash command into a freshly-spawned pane would
|
|
render as an unexpected "Command model X" item before the
|
|
user has sent anything. Default ``False`` preserves the
|
|
user-driven picker / ``/model`` behaviour where the live
|
|
forward IS the desired feedback.
|
|
:param archived: New archived state. ``True`` archives (hides the
|
|
session from the default sidebar listing), ``False`` unarchives,
|
|
``None`` leaves unchanged. Owner-only (unlike ``title``, which
|
|
needs only edit access).
|
|
"""
|
|
|
|
runner_id: str | None = None
|
|
title: str | None = None
|
|
labels: dict[str, str] | None = None
|
|
reasoning_effort: str | None = None
|
|
model_override: str | None = None
|
|
collaboration_mode: str | None = None
|
|
cost_control_mode_override: str | None = None
|
|
external_session_id: str | None = None
|
|
terminal_launch_args: list[str] | None = None
|
|
archived: bool | None = None
|
|
silent: bool = False
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class CodexGoalObject(BaseModel):
|
|
"""
|
|
Current Codex goal state for a Codex-native session.
|
|
|
|
Mirrors Codex app-server's ``ThreadGoal`` shape using Omnigent's
|
|
snake-case API convention. ``created_at`` and ``updated_at`` are optional
|
|
because older app-server documentation examples omit them even though the
|
|
current protocol includes them.
|
|
|
|
:param thread_id: Codex app-server thread id, e.g. ``"thr_123"``.
|
|
:param objective: Goal objective text, e.g.
|
|
``"Finish the migration and keep tests green"``.
|
|
:param status: Raw Codex goal lifecycle status, e.g. ``"active"``.
|
|
:param token_budget: Optional token budget, e.g. ``40000``.
|
|
``None`` means no explicit budget is set.
|
|
:param tokens_used: Tokens spent while pursuing this goal, e.g. ``1024``.
|
|
:param time_used_seconds: Wall-clock seconds spent on this goal,
|
|
e.g. ``60``.
|
|
:param created_at: Unix timestamp when the goal was created, e.g.
|
|
``1776272400``. ``None`` when not provided by Codex.
|
|
:param updated_at: Unix timestamp when the goal was last updated, e.g.
|
|
``1776272460``. ``None`` when not provided by Codex.
|
|
"""
|
|
|
|
thread_id: str
|
|
objective: str
|
|
status: str
|
|
token_budget: Annotated[int, Strict(), Field(gt=0)] | None = None
|
|
tokens_used: Annotated[int, Strict(), Field(ge=0)]
|
|
time_used_seconds: Annotated[int, Strict(), Field(ge=0)]
|
|
created_at: int | None = None
|
|
updated_at: int | None = None
|
|
|
|
|
|
class CodexGoalResponse(BaseModel):
|
|
"""
|
|
Response body for reading or setting a Codex-native session goal.
|
|
|
|
:param goal: Current goal state, or ``None`` when the session has no
|
|
persisted Codex goal.
|
|
"""
|
|
|
|
goal: CodexGoalObject | None
|
|
|
|
|
|
class SetCodexGoalRequest(BaseModel):
|
|
"""
|
|
Request body for ``PUT /v1/sessions/{id}/codex_goal``.
|
|
|
|
:param objective: Goal objective text, e.g.
|
|
``"Finish the migration and keep tests green"``. Must be non-empty
|
|
after trimming and no longer than 4000 characters, matching Codex
|
|
app-server's goal contract.
|
|
:param token_budget: Optional positive token budget, e.g. ``40000``.
|
|
Explicit JSON ``null`` clears the Codex goal budget; omitting the
|
|
field leaves it absent from the forwarded request.
|
|
:param status: Optional user-selected goal status. ``"active"`` starts or
|
|
resumes the goal, and ``"paused"`` stores it paused. Omit this field
|
|
to preserve Codex's current lifecycle state.
|
|
"""
|
|
|
|
objective: str = Field(min_length=1, max_length=4000)
|
|
token_budget: Annotated[int, Strict(), Field(gt=0)] | None = None
|
|
status: Literal["active", "paused"] | None = None
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class UpdateCodexGoalStatusRequest(BaseModel):
|
|
"""
|
|
Request body for ``PATCH /v1/sessions/{id}/codex_goal/status``.
|
|
|
|
Codex app-server represents pause/resume as ``thread/goal/set`` status
|
|
updates. Omnigent exposes the two user-driven transitions explicitly:
|
|
``"paused"`` pauses an active goal, and ``"active"`` resumes a paused,
|
|
blocked, or usage-limited goal.
|
|
|
|
:param status: Target Codex goal status, either ``"paused"`` or
|
|
``"active"``.
|
|
"""
|
|
|
|
status: Literal["active", "paused"]
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class ClearCodexGoalResponse(BaseModel):
|
|
"""
|
|
Response body for ``DELETE /v1/sessions/{id}/codex_goal``.
|
|
|
|
:param cleared: ``True`` when Codex removed an existing goal; ``False``
|
|
when no goal was present.
|
|
"""
|
|
|
|
cleared: bool
|
|
|
|
|
|
class SessionForkRequest(BaseModel):
|
|
"""
|
|
Request body for ``POST /v1/sessions/{source_id}/fork``.
|
|
|
|
Creates a deep copy of an existing session's items into a new
|
|
session. All fields are optional.
|
|
|
|
:param title: Title for the forked session. When ``None``, the
|
|
server derives ``"Fork of <source_title>"``.
|
|
:param agent_id: Built-in agent to bind the fork to, switching it
|
|
away from the source's agent/harness (e.g. fork a Claude session
|
|
into a Codex one, or a Claude-SDK session into Claude Code). When
|
|
``None``, the fork keeps the source's agent. Must be a built-in
|
|
agent (one listed by ``GET /v1/agents``).
|
|
:param up_to_response_id: Truncation point for the copied history,
|
|
e.g. ``"resp_abc123"``. When set, only items up to and including
|
|
the last item of that response are copied — items after it are
|
|
dropped from the fork. When ``None`` (default), the full history
|
|
is copied.
|
|
"""
|
|
|
|
title: str | None = None
|
|
agent_id: str | None = None
|
|
up_to_response_id: str | None = None
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class ReadStatePutRequest(BaseModel):
|
|
"""
|
|
Request body for ``PUT /v1/sessions/{session_id}/read-state``.
|
|
|
|
Sets the *calling user's* read tracking for one session. Mirrors the
|
|
two values the web client keeps per session: a "last seen" wall-clock
|
|
baseline (seconds since epoch) and an explicit "marked unread"
|
|
override. The unread dot shows when ``updated_at > last_seen`` and the
|
|
session is finished; ``unread`` separately pins the override so the
|
|
thread the user is *viewing* (or a running one) still surfaces the dot
|
|
where the automatic "seen" logic would otherwise suppress it.
|
|
|
|
:param last_seen: Wall-clock baseline in seconds, e.g. ``1717000000``.
|
|
Marking seen sets this to "now"; marking unread pins it to
|
|
``updated_at - 1`` so the row reads unseen.
|
|
:param unread: Whether this session is explicitly flagged unread for
|
|
the caller.
|
|
"""
|
|
|
|
last_seen: int
|
|
unread: bool
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class SessionSwitchAgentRequest(BaseModel):
|
|
"""
|
|
Request body for ``POST /v1/sessions/{id}/switch-agent``.
|
|
|
|
Rebinds an existing session in place to a different agent/harness,
|
|
keeping the same session (transcript, comments, files, workspace).
|
|
Unlike fork, no new session is created.
|
|
|
|
:param agent_id: Built-in agent to switch the session to, e.g.
|
|
``"ag_builtin_codex"``. Must be a built-in agent (one listed by
|
|
``GET /v1/agents``) and different from the session's current
|
|
agent.
|
|
"""
|
|
|
|
agent_id: str
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class SessionListItem(BaseModel):
|
|
"""
|
|
Lightweight session summary for ``GET /v1/sessions`` list responses.
|
|
|
|
Same shape as :class:`SessionResponse` minus ``items``.
|
|
|
|
:param id: Session/conversation identifier,
|
|
e.g. ``"conv_abc123"``.
|
|
:param agent_id: Durable identifier of the bound agent.
|
|
:param agent_name: Human-readable name of the bound agent,
|
|
e.g. ``"research-agent"``. ``None`` when the agent row
|
|
cannot be found.
|
|
:param status: Derived session lifecycle status.
|
|
:param created_at: Unix epoch seconds of creation.
|
|
:param updated_at: Unix epoch seconds of last update.
|
|
:param title: Optional human-readable title.
|
|
:param labels: Session-scoped guardrails labels.
|
|
:param runner_id: Runner currently bound to the session.
|
|
:param host_id: Host that launched the runner for this session.
|
|
:param runner_online: Strict runner liveness — ``True`` iff a
|
|
runner tunnel is currently registered for this session.
|
|
Matches ``GET /health``'s ``runner_online`` value. Strict:
|
|
a dead runner on a live host reads ``False`` here (no
|
|
host-relaunch optimism folded in), unlike the legacy
|
|
conflated value. ``None`` when the server has no runner
|
|
liveness lookup wired.
|
|
:param host_online: Whether the session's host tunnel is live
|
|
(status online and fresh within the host liveness TTL).
|
|
``None`` when the session has no ``host_id`` (CLI/local).
|
|
Distinguishes "runner down but host can relaunch" from
|
|
"host offline" for the open-session view; not used by the
|
|
sidebar.
|
|
:param reasoning_effort: Per-session reasoning-effort hint.
|
|
:param permission_level: The requesting user's numeric
|
|
permission level on this session: ``1`` = read, ``2`` =
|
|
edit, ``3`` = manage. ``None`` when permissions are
|
|
disabled.
|
|
:param owner: The user_id of the session owner, or ``None``
|
|
when permissions are disabled. Included so the sidebar
|
|
can display the owner without a separate API call.
|
|
:param external_session_id: Runtime-native session id this
|
|
conversation wraps, e.g. a Claude Code session uuid for
|
|
``omnigent claude`` sessions. ``None`` for regular
|
|
AP-only conversations. Lets the sidebar / picker render
|
|
a runtime badge without a follow-up GET.
|
|
:param pending_elicitations_count: Number of approval prompts
|
|
currently waiting on this session. Powers the sidebar's
|
|
"needs attention" badge so a user with several sessions
|
|
running can tell which ones are blocked on them without
|
|
opening each chat. Sourced from the Omnigent server's in-memory
|
|
:mod:`omnigent.runtime.pending_elicitations` index,
|
|
which mirrors every ``response.elicitation_request`` event
|
|
passing through ``session_stream`` and decrements when a
|
|
verdict is dispatched. ``0`` when the session has no
|
|
outstanding elicitations.
|
|
:param workspace: Absolute path on disk where the runner cd's,
|
|
e.g. ``"/Users/corey/universe/src/foo"``. ``None`` for
|
|
sessions that haven't been bound to a host workspace.
|
|
:param git_branch: 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;
|
|
``None`` otherwise. The Web UI uses a non-``None`` value to
|
|
offer the "delete local branch" cleanup checkbox on session
|
|
delete. See designs/SESSION_GIT_WORKTREE.md.
|
|
:param archived: Whether the session is archived. Archived
|
|
sessions are returned by ``GET /v1/sessions`` only when the
|
|
request passes ``include_archived=true``; the sidebar groups
|
|
them into a dedicated "Archived" section. ``False`` for
|
|
normal sessions.
|
|
:param comments_count: Total number of review comments (any
|
|
status) on this session. Together with
|
|
``comments_updated_at`` it forms a change fingerprint: an
|
|
add or edit bumps the timestamp, a delete changes the count,
|
|
so the web client can invalidate its cached comment list
|
|
when either field changes in a ``WS /v1/sessions/updates``
|
|
frame. ``0`` when the session has no comments or the server
|
|
has no comment store wired.
|
|
:param comments_updated_at: Unix epoch **microseconds** of the
|
|
most recently mutated comment on this session (max
|
|
``updated_at`` across its comments). Microsecond precision
|
|
keeps back-to-back mutations within one second
|
|
distinguishable while staying an exact integer in JavaScript;
|
|
clients only compare it for change. ``None`` when the session
|
|
has no comments or the server has no comment store wired.
|
|
:param viewer_last_seen: The *requesting user's* "last seen"
|
|
wall-clock baseline in seconds for this session, or ``None``
|
|
when they have never seen it. Per-viewer (built from the
|
|
server's in-memory per-user read-state, written by
|
|
``PUT /v1/sessions/{id}/read-state``); the unread dot shows
|
|
when ``updated_at > viewer_last_seen`` and the session is
|
|
finished. In-memory only — resets on a server restart.
|
|
:param viewer_unread: Whether the *requesting user* explicitly
|
|
marked this session unread. Per-viewer; lifts the active-row
|
|
dot suppression on the client. ``False`` by default.
|
|
:param search_snippet: Excerpt of the chat content that matched the
|
|
request's ``search_query``, centered on the match with ``…``
|
|
marking elided ends, so the search UI can show *where* a session
|
|
matched in its body. Present whenever the query hit an item body
|
|
(even if the title also matched); ``None`` on non-search reads and
|
|
when only the title matched.
|
|
"""
|
|
|
|
id: str
|
|
agent_id: str
|
|
agent_name: str | None = None
|
|
status: Literal["idle", "running", "waiting", "failed"]
|
|
created_at: int
|
|
updated_at: int
|
|
title: str | None = None
|
|
labels: dict[str, str] = Field(default_factory=dict)
|
|
runner_id: str | None = None
|
|
host_id: str | None = None
|
|
runner_online: bool | None = None
|
|
host_online: bool | None = None
|
|
reasoning_effort: str | None = None
|
|
permission_level: int | None = None
|
|
owner: str | None = None
|
|
external_session_id: str | None = None
|
|
pending_elicitations_count: int = 0
|
|
workspace: str | None = None
|
|
git_branch: str | None = None
|
|
archived: bool = False
|
|
comments_count: int = 0
|
|
comments_updated_at: int | None = None
|
|
viewer_last_seen: int | None = None
|
|
viewer_unread: bool = False
|
|
search_snippet: str | None = None
|
|
|
|
|
|
class SessionList(BaseModel):
|
|
"""Paginated list of sessions; ``data`` is a page of ``SessionListItem``."""
|
|
|
|
object: Literal["list"] = "list"
|
|
data: list[SessionListItem] = Field(default_factory=list)
|
|
first_id: str | None = None
|
|
last_id: str | None = None
|
|
has_more: bool = False
|
|
|
|
|
|
class ChildSessionList(BaseModel):
|
|
"""Paginated list of child sessions; ``data`` is a page of ``ChildSessionSummary``."""
|
|
|
|
object: Literal["list"] = "list"
|
|
data: list[ChildSessionSummary] = Field(default_factory=list)
|
|
first_id: str | None = None
|
|
last_id: str | None = None
|
|
has_more: bool = False
|
|
|
|
|
|
# ── Permissions ────────────────────────────────────────────────────
|
|
|
|
|
|
class GrantPermissionRequest(BaseModel):
|
|
"""
|
|
Request body for ``PUT /v1/sessions/{id}/permissions``.
|
|
|
|
:param user_id: The user to grant access to, e.g.
|
|
``"alice@example.com"`` or ``"__public__"`` for public
|
|
read access.
|
|
:param level: Numeric permission level: ``1`` = read,
|
|
``2`` = edit, ``3`` = manage.
|
|
"""
|
|
|
|
user_id: str
|
|
level: int = Field(ge=1, le=3)
|
|
|
|
|
|
class PermissionObject(BaseModel):
|
|
"""
|
|
API representation of a session permission grant.
|
|
|
|
:param user_id: The grantee, e.g. ``"alice@example.com"``.
|
|
:param conversation_id: The session, e.g.
|
|
``"conv_abc123"``.
|
|
:param level: Numeric permission level (1=read, 2=edit,
|
|
3=manage).
|
|
"""
|
|
|
|
user_id: str
|
|
conversation_id: str
|
|
level: int
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
# STREAM EVENTS — typed Pydantic union for SSE event boundary
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
#
|
|
# Single source of truth for the omnigent SSE event stream. Every
|
|
# event the server emits over its two SSE endpoints is modeled below
|
|
# as a Pydantic class with a ``type: Literal[...]`` discriminator, and
|
|
# the ``ServerStreamEvent`` annotated union routes raw event dicts to
|
|
# the right concrete model. Server, runtime, REPL/TUI, and SDK all
|
|
# import these names from this module so wire-name renames and
|
|
# payload changes are a one-edit change.
|
|
#
|
|
# The SSE endpoint is:
|
|
#
|
|
# * ``GET /v1/sessions/{id}/stream`` — session live-tail (multiplexes
|
|
# the underlying response stream and surfaces queue/interrupt
|
|
# semantics).
|
|
#
|
|
# Two event families coexist:
|
|
#
|
|
# * ``session.*`` — session-scoped lifecycle events
|
|
# (:class:`SessionStatusEvent`, :class:`SessionInputConsumedEvent`,
|
|
# :class:`SessionInterruptedEvent`, :class:`SessionCreatedEvent`).
|
|
# * ``response.*`` — pass-through Responses-API events emitted by the
|
|
# executor; the session stream multiplexes them unchanged.
|
|
#
|
|
# Channel split (per ``designs/session_rearchitecture.md`` §3 "Two
|
|
# channels"). Each event variant is conceptually either *transient*
|
|
# or *persistent*:
|
|
#
|
|
# * Transient (SSE-only) — text/reasoning deltas, turn lifecycle
|
|
# events, ``session.*`` lifecycle events, retry/heartbeat/error
|
|
# signals, ``approval_required``. Fire-and-forget on the SSE
|
|
# stream — NOT persisted.
|
|
# * Persistent (POST + SSE replay) — assistant messages, tool calls,
|
|
# tool results, and compaction summaries. Persist-then-publish is
|
|
# enforced inside ``_persist_and_stream``.
|
|
#
|
|
# Wire-shape note: the server today emits some events with a flat
|
|
# shape (``{"type": ..., <fields>}``) and others with a nested
|
|
# ``{"type": ..., "data": {...}}`` envelope. The Pydantic models
|
|
# below match the wire shapes verbatim — see each model's docstring
|
|
# for the emit site reference.
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
# ── Module-level constants (rule 34) ──────────────────────────────
|
|
|
|
# Forward-compatibility note: every event model uses ``ConfigDict(
|
|
# extra="ignore")`` so harnesses (or AP) can add new fields to an
|
|
# event in a future contract revision without breaking older
|
|
# parsers — see ``designs/SERVER_HARNESS_CONTRACT.md`` §Validation
|
|
# discipline (loose by default).
|
|
|
|
|
|
class _SSEEventBase(BaseModel):
|
|
"""
|
|
Common base for every SSE event payload model.
|
|
|
|
All events share two ambient fields:
|
|
|
|
- ``type``: the event-type discriminator literal (defined per
|
|
subclass so :data:`ServerStreamEvent` can dispatch).
|
|
- ``sequence_number``: monotonic per-stream sequence number
|
|
assigned by the SSE serializer at emit time
|
|
(``_format_sse`` in ``omnigent/server/routes/sessions.py``).
|
|
Producers leave it ``None``; the route serializer populates
|
|
it on the wire. ``None`` on session-scoped events emitted
|
|
directly by the runtime (the session stream does not number
|
|
events).
|
|
|
|
Subclasses MUST declare ``type`` as ``Literal[...]`` so the
|
|
discriminated-union machinery can route incoming dicts. The
|
|
``model_config`` is forward-compatible — see the module
|
|
docstring for the rationale.
|
|
|
|
:param sequence_number: Per-stream monotonic counter assigned
|
|
by the SSE serializer, e.g. ``42``. ``None`` on the
|
|
producer side (before serialization) and on session-scoped
|
|
events that the runtime publishes directly without
|
|
sequencing.
|
|
"""
|
|
|
|
sequence_number: int | None = None
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
|
|
# ── Session-scoped events (session.*) ──────────────────────────────
|
|
|
|
|
|
class SessionStatusEvent(_SSEEventBase):
|
|
"""
|
|
Session lifecycle status transition.
|
|
|
|
Emitted by the runtime / session route handler at every
|
|
transition between ``launching`` / ``running`` / ``waiting`` /
|
|
``idle`` / ``failed``. Wire shape is
|
|
FLAT (not enveloped): ``{"type": "session.status",
|
|
"conversation_id": "...", "status": "...",
|
|
"sequence_number": null}``.
|
|
|
|
The ``waiting`` value is emitted by the runtime's parent agent
|
|
loop when it parks on the ``async_work_complete`` drain
|
|
(``_drain_async_completions(block_for_one=True)`` in
|
|
``omnigent/runtime/workflow.py``) — i.e. while the parent
|
|
turn is suspended waiting for background tools or sub-agents
|
|
to complete. Per the session-rearchitecture spec §3
|
|
("Event types and direction"), ``waiting`` is the
|
|
session-status companion of the spec's ``turn.waiting``
|
|
transient — clients should render the session as actively
|
|
blocked-on-async-work, distinct from ``running``. When the
|
|
drain wakes (a child completed), the runtime emits a follow-up
|
|
``running`` to resume.
|
|
|
|
:param type: Always ``"session.status"``.
|
|
:param conversation_id: The conversation/session identifier
|
|
whose status changed, e.g. ``"conv_abc123"``.
|
|
:param status: New session status. ``"launching"`` (session or
|
|
child task created, but no concrete harness start observed),
|
|
``"idle"`` (no loop running), ``"running"`` (loop executing),
|
|
``"waiting"`` (parent turn parked on the async-work drain), or
|
|
``"failed"`` (terminal failure).
|
|
:param response_id: Optional active response id for terminal-backed
|
|
integrations, e.g. ``"codex_turn_abc123"``. Clients use it to
|
|
associate coarse session status edges with the assistant bubble
|
|
they describe. ``None`` for ordinary in-process runtime edges.
|
|
:param error: Machine-readable failure detail, present only
|
|
when ``status == "failed"``. Carries the message the
|
|
runner attached when a turn died — most importantly a
|
|
SETUP-phase failure (spec resolution, spawn-env build)
|
|
that ends the turn before any ``response.failed`` event
|
|
is emitted. ``None`` for every non-failed transition.
|
|
Clients render ``error.message`` as the terminal error
|
|
line; without it a setup failure shows as a silent end.
|
|
|
|
Category: **transient** (SSE-only). Status is rederived on
|
|
reconnect from the cached last-relayed turn lifecycle event
|
|
or by re-querying the runner; not persisted by the runtime.
|
|
"""
|
|
|
|
type: Literal["session.status"]
|
|
conversation_id: str
|
|
status: Literal["idle", "launching", "running", "waiting", "failed"]
|
|
response_id: str | None = None
|
|
error: ErrorDetail | None = None
|
|
background_task_count: int | None = None
|
|
|
|
|
|
class SessionUsageEvent(_SSEEventBase):
|
|
"""
|
|
Token-usage update from a terminal-backed integration.
|
|
|
|
Emitted after an ``external_session_usage`` POST from an
|
|
out-of-AP runtime (e.g. the ``omnigent claude`` transcript
|
|
forwarder). Either field may be absent; clients should leave
|
|
cached values untouched for missing fields.
|
|
|
|
:param type: Always ``"session.usage"``.
|
|
:param conversation_id: Session identifier.
|
|
:param context_tokens: ``input + cache_creation + cache_read``
|
|
from the latest assistant ``message.usage``. ``None`` on a
|
|
window-only broadcast.
|
|
:param context_window: Resolved window in tokens (e.g. 200_000
|
|
normally, 1_000_000 with ``opus[1m]`` / ``sonnet[1m]``).
|
|
``None`` on a tokens-only broadcast.
|
|
:param total_cost_usd: Cumulative session spend in USD after this
|
|
update, e.g. ``0.42`` — the server-computed total the
|
|
cost-budget policy gates on. Present **only when the session
|
|
is priced**; omitted (``None``, stripped by ``exclude_none``)
|
|
when unpriced or on a broadcast that carries no cost change,
|
|
so the client keeps its prior value (the snapshot seeds the
|
|
initial "—" for an unpriced session). Once a session is priced
|
|
the total only grows, so it never reverts to unpriced.
|
|
:param usage_by_model: Per-model breakdown of the same subtree usage
|
|
after this update, keyed by raw harness model id, e.g.
|
|
``{"claude-sonnet-4-6": ModelUsage(input_tokens=12000, ...)}``.
|
|
``None`` (stripped by ``exclude_none``) on a broadcast that carries
|
|
no per-model change, so the client keeps its cached map.
|
|
|
|
Category: **transient** (SSE-only). On reconnect, clients seed
|
|
the ring from the session snapshot's ``last_total_tokens`` and
|
|
``context_window``, the cost indicator from ``total_cost_usd``,
|
|
and the per-model token breakdown from ``usage_by_model``.
|
|
"""
|
|
|
|
type: Literal["session.usage"]
|
|
conversation_id: str
|
|
context_tokens: int | None = None
|
|
context_window: int | None = None
|
|
total_cost_usd: float | None = None
|
|
usage_by_model: dict[str, ModelUsage] | None = None
|
|
|
|
|
|
class SessionModelEvent(_SSEEventBase):
|
|
"""
|
|
Active-model update from a terminal-backed integration.
|
|
|
|
Emitted after an ``external_model_change`` POST from the
|
|
``omnigent claude`` transcript forwarder when the model is
|
|
switched inside the Claude Code terminal (a ``/model`` command or
|
|
the in-TUI picker). Lets the web model picker reflect a TUI-side
|
|
switch without a reload.
|
|
|
|
:param type: Always ``"session.model"``.
|
|
:param conversation_id: Session identifier, e.g. ``"conv_abc123"``.
|
|
:param model: Tier alias the session is now on, e.g. ``"opus"`` —
|
|
Claude Code's version-agnostic alias, matching the picker's
|
|
vocabulary (not a pinned ``"claude-opus-4-8"`` id).
|
|
|
|
Category: **transient** (SSE-only). The server also writes
|
|
``model_override`` on the conversation, so on reconnect clients
|
|
restore the selection from the snapshot's ``model_override`` rather
|
|
than from a replayed event.
|
|
"""
|
|
|
|
type: Literal["session.model"]
|
|
conversation_id: str
|
|
model: str
|
|
|
|
|
|
class SessionReasoningEffortEvent(_SSEEventBase):
|
|
"""
|
|
Active reasoning-effort update from a terminal-backed integration.
|
|
|
|
Emitted after an ``external_reasoning_effort_change`` POST from a native
|
|
terminal forwarder when the user changes the thinking level inside the
|
|
terminal UI. Lets the web effort picker reflect a TUI-side switch without
|
|
a reload.
|
|
|
|
:param type: Always ``"session.reasoning_effort"``.
|
|
:param conversation_id: Session identifier, e.g. ``"conv_abc123"``.
|
|
:param reasoning_effort: Reasoning effort now active for the session, e.g.
|
|
``"medium"``, or ``None`` when Codex cleared to its default.
|
|
|
|
Category: **transient** (SSE-only). The server also writes
|
|
``reasoning_effort`` on the conversation, so on reconnect clients restore
|
|
the selection from the session snapshot rather than from a replayed event.
|
|
"""
|
|
|
|
type: Literal["session.reasoning_effort"]
|
|
conversation_id: str
|
|
reasoning_effort: str | None = None
|
|
|
|
|
|
class SessionCollaborationModeEvent(_SSEEventBase):
|
|
"""
|
|
Active collaboration-mode update from a Codex-native session.
|
|
|
|
Emitted after the web UI toggles Codex collaboration mode, and after the
|
|
Codex forwarder observes a ``thread/settings/updated`` notification from
|
|
the native Codex TUI. Lets connected clients show a clear Plan-mode
|
|
indicator without a reload.
|
|
|
|
:param type: Always ``"session.collaboration_mode"``.
|
|
:param conversation_id: Session identifier, e.g. ``"conv_abc123"``.
|
|
:param mode: The active collaboration mode string, e.g. ``"plan"`` or
|
|
``"default"``.
|
|
|
|
Category: **transient** (SSE-only). The server also writes
|
|
``omnigent.codex_native.collaboration_mode`` on the conversation labels,
|
|
so reconnect clients restore the same state from the session snapshot.
|
|
"""
|
|
|
|
type: Literal["session.collaboration_mode"]
|
|
conversation_id: str
|
|
mode: str
|
|
|
|
|
|
class SessionAgentChangedEvent(_SSEEventBase):
|
|
"""
|
|
Bound-agent change on a live session.
|
|
|
|
Emitted by the switch-agent route after the session's agent binding
|
|
is rewritten in place. Connected clients re-derive their cached
|
|
session state (harness presentation labels, bound agent id/name)
|
|
from a fresh snapshot — the chat UI's native-vs-SDK message
|
|
lifecycle depends on those labels, so a stale cache drops the first
|
|
post-switch message (it reappears only when the transcript
|
|
round-trip lands).
|
|
|
|
:param type: Always ``"session.agent_changed"``.
|
|
:param conversation_id: Session identifier, e.g. ``"conv_abc123"``.
|
|
:param agent_id: The session-scoped clone now bound to the session,
|
|
e.g. ``"ag_abc123"``.
|
|
:param agent_name: Display name of the agent the session now runs,
|
|
e.g. ``"claude-native-ui"``. Deliberately the clean target-agent
|
|
name — not the clone row's ``"… (switch ag_…)"`` disambiguation
|
|
name — because clients render it verbatim.
|
|
|
|
Category: **transient** (SSE-only). The switch is persisted on the
|
|
conversation row, so on reconnect clients read the new binding from
|
|
the session snapshot rather than from a replayed event.
|
|
"""
|
|
|
|
type: Literal["session.agent_changed"]
|
|
conversation_id: str
|
|
agent_id: str
|
|
agent_name: str
|
|
|
|
|
|
class SessionTodosEvent(_SSEEventBase):
|
|
"""
|
|
Todo-list update from a Claude Code terminal-backed session.
|
|
|
|
Emitted after an ``external_session_todos`` POST from the
|
|
``omnigent claude`` transcript forwarder, which captures todo
|
|
updates via ``PostToolUse``/``TodoWrite`` hook events from Claude
|
|
Code and forwards them to the Omnigent server. Lets web render a
|
|
live todo panel in the right column without polling.
|
|
|
|
:param type: Always ``"session.todos"``.
|
|
:param conversation_id: Session identifier,
|
|
e.g. ``"conv_abc123"``.
|
|
:param todos: Current todo items read from Claude's todo file.
|
|
Each entry is a raw dict with ``content`` (str),
|
|
``status`` (``"pending"`` | ``"in_progress"`` |
|
|
``"completed"``), and ``activeForm`` (str, the gerund form)
|
|
keys, e.g. ``[{"content": "Fix the bug", "status":
|
|
"in_progress", "activeForm": "Fixing the bug"}]``.
|
|
|
|
Category: **transient** (SSE-only). On reconnect, clients seed
|
|
the panel from the session snapshot's ``todos`` field, which is
|
|
populated by ``_session_todos_cache`` at snapshot build time.
|
|
"""
|
|
|
|
type: Literal["session.todos"]
|
|
conversation_id: str
|
|
todos: list[dict[str, Any]]
|
|
|
|
|
|
class SessionTerminalPendingEvent(_SSEEventBase):
|
|
"""
|
|
Terminal spin-up status for a terminal-first session.
|
|
|
|
Two sources emit this event:
|
|
|
|
1. The Omnigent server at ``POST /v1/sessions`` for host-launched
|
|
terminal-first sessions — the earliest possible point, before
|
|
the runner even starts, so the spinner appears immediately on
|
|
session create rather than after the runner boots.
|
|
2. The Omnigent relay when the runner's ``session.terminal_pending`` frame
|
|
arrives — covers non-host-launched sessions (e.g. server-dispatched
|
|
sub-agents) and carries the authoritative ``pending=False`` clear
|
|
emitted by the runner's ``finally`` block.
|
|
|
|
Together they allow web to show a spinner on the Terminal pill
|
|
while the backend boots the terminal instead of a silent greyed-out
|
|
button, and to distinguish "still starting up" from "no terminal"
|
|
(killed or never created).
|
|
|
|
:param type: Always ``"session.terminal_pending"``.
|
|
:param conversation_id: Session identifier,
|
|
e.g. ``"conv_abc123"``.
|
|
:param pending: ``True`` while the terminal is being created;
|
|
``False`` once it lands or auto-create fails.
|
|
|
|
Category: **transient** (SSE-only). On reconnect, clients seed the
|
|
spinner from the session snapshot's ``terminal_pending`` field,
|
|
which is populated by ``_session_terminal_pending_cache`` at
|
|
snapshot build time.
|
|
"""
|
|
|
|
type: Literal["session.terminal_pending"]
|
|
conversation_id: str
|
|
pending: bool
|
|
|
|
|
|
class SessionSandboxStatusEvent(_SSEEventBase):
|
|
"""
|
|
Managed-sandbox launch progress for a ``host_type="managed"`` session.
|
|
|
|
A managed create returns before its sandbox exists; the Omnigent
|
|
server emits this event as the background launch pipeline advances
|
|
so the Web UI can show live provisioning progress on the session
|
|
page instead of a silent dead chat: sandbox provision → repository
|
|
clone → host startup → runner connect → ready, or a terminal
|
|
failure with the reason.
|
|
|
|
:param type: Always ``"session.sandbox_status"``.
|
|
:param conversation_id: Session identifier,
|
|
e.g. ``"conv_abc123"``.
|
|
:param stage: The launch stage just entered, e.g.
|
|
``"provisioning"`` — see :class:`SandboxStatus` for the full
|
|
pipeline order.
|
|
:param error: Failure detail when ``stage == "failed"``, e.g.
|
|
``"managed sandbox launch failed: spend limit reached"``.
|
|
``None`` otherwise.
|
|
|
|
Category: **transient** (SSE-only). On reconnect, clients seed the
|
|
progress indicator from the session snapshot's ``sandbox_status``
|
|
field, which is populated by ``_session_sandbox_status_cache`` at
|
|
snapshot build time.
|
|
"""
|
|
|
|
type: Literal["session.sandbox_status"]
|
|
conversation_id: str
|
|
stage: SandboxLaunchStage
|
|
error: str | None = None
|
|
|
|
|
|
class McpServerStartup(BaseModel):
|
|
"""
|
|
One MCP server's startup state within a ``session.mcp_startup`` event.
|
|
|
|
:param status: Latest startup state reported by the harness, mirroring
|
|
Codex's ``McpServerStartupState`` enum.
|
|
:param error: Failure detail when ``status == "failed"``, e.g.
|
|
``"handshaking with MCP server failed"``. ``None`` otherwise.
|
|
"""
|
|
|
|
status: Literal["starting", "ready", "failed", "cancelled"]
|
|
error: str | None = None
|
|
|
|
|
|
class SessionMcpStartupEvent(_SSEEventBase):
|
|
"""
|
|
Per-MCP-server startup progress for a native harness session.
|
|
|
|
A codex-native session brings up its configured MCP servers when its
|
|
Codex thread starts; slow or failing servers previously left the web
|
|
session looking hung with no signal. The native forwarder mirrors
|
|
Codex's ``mcpServer/startupStatus/updated`` notifications as
|
|
``external_mcp_startup`` posts, republished here so the web UI can
|
|
show which servers are still starting and which failed or were
|
|
cancelled.
|
|
|
|
:param type: Always ``"session.mcp_startup"``.
|
|
:param conversation_id: Session identifier,
|
|
e.g. ``"conv_abc123"``.
|
|
:param servers: Latest per-server startup map, e.g.
|
|
``{"safe": {"status": "starting", "error": None}}``.
|
|
|
|
Category: **transient** (SSE + snapshot cache). Not persisted; a
|
|
client connecting mid-startup seeds from the session snapshot's
|
|
``mcp_startup`` field and updates live off this event.
|
|
"""
|
|
|
|
type: Literal["session.mcp_startup"]
|
|
conversation_id: str
|
|
servers: dict[str, McpServerStartup]
|
|
|
|
|
|
class SessionSkillsEvent(_SSEEventBase):
|
|
"""
|
|
Signal that a session's runner-owned skills have resolved.
|
|
|
|
Skills are discovered against the bound runner's filesystem and
|
|
fetched off the session-snapshot hot path: the snapshot kicks a
|
|
single background fetch (``_load_runner_skills`` in
|
|
``omnigent/server/routes/sessions.py``) and serves ``[]`` until
|
|
it lands. This event fires the moment that background fetch
|
|
populates the per-session skills cache, so a connected web client
|
|
can re-read the snapshot and fill its slash-command menu instead
|
|
of waiting for the next bind.
|
|
|
|
Carries no payload beyond the conversation id — it is a "skills
|
|
are ready, re-read the snapshot" nudge, mirroring the
|
|
invalidate-then-refetch shape used by
|
|
:class:`SessionChangedFilesInvalidatedEvent`. The snapshot's
|
|
``skills`` field (now cache-backed) stays the source of truth.
|
|
|
|
:param type: Always ``"session.skills"``.
|
|
:param conversation_id: Session identifier,
|
|
e.g. ``"conv_abc123"``.
|
|
|
|
Category: **transient** (SSE-only). On reconnect, clients seed
|
|
the menu from the session snapshot's ``skills`` field, which is
|
|
populated by the runner-skills cache at snapshot build time.
|
|
"""
|
|
|
|
type: Literal["session.skills"]
|
|
conversation_id: str
|
|
|
|
|
|
class SessionModelOptionsEvent(_SSEEventBase):
|
|
"""
|
|
Signal that a codex-native session's model catalog has resolved.
|
|
|
|
Model options are fetched from the bound runner's live
|
|
Codex app-server via ``model/list`` and cached on the session
|
|
snapshot. The initial snapshot can return an empty list while
|
|
this background fetch is in flight; this event tells connected
|
|
clients to re-read the snapshot and apply its now-populated
|
|
``model_options``.
|
|
|
|
Carries no payload beyond the conversation id. The snapshot's
|
|
``model_options`` field remains the source of truth.
|
|
|
|
:param type: Always ``"session.model_options"``.
|
|
:param conversation_id: Session identifier,
|
|
e.g. ``"conv_abc123"``.
|
|
|
|
Category: **transient** (SSE-only). On reconnect, clients seed
|
|
Codex model / effort controls from the session snapshot.
|
|
"""
|
|
|
|
type: Literal["session.model_options"]
|
|
conversation_id: str
|
|
|
|
|
|
class SessionInputConsumedPayload(BaseModel):
|
|
"""
|
|
Inner payload of a :class:`SessionInputConsumedEvent`.
|
|
|
|
Emitted by the sessions route handler at the moment a client
|
|
input is persisted into ``conversation_items``. Carries the
|
|
persisted-item shape so clients can render the input (e.g.
|
|
the user's message bubble) at the moment of acceptance.
|
|
|
|
:param item_id: Stable identifier of the conversation item
|
|
just persisted, e.g. ``"item_abc123"``.
|
|
:param type: The item type discriminator — ``"message"`` for
|
|
user messages, ``"function_call_output"`` for tool
|
|
results, etc. Mirrors
|
|
:class:`omnigent.server.schemas.SessionEventInput`'s
|
|
``type`` field.
|
|
:param data: Decoded item payload, e.g.
|
|
``{"role": "user", "content": [{"type": "input_text",
|
|
"text": "Hello"}]}``. Heterogeneous and ``type``-specific.
|
|
:param created_by: Email of the human actor who posted the item,
|
|
e.g. ``"alice@example.com"``. ``None`` for agent/tool/system
|
|
items and single-user mode. Mirrors
|
|
:meth:`ConversationItem.to_api_dict` for live attribution.
|
|
:param cleared_pending_id: When this consumed message drains a
|
|
:mod:`omnigent.runtime.pending_inputs` entry (a native-
|
|
terminal web message round-tripping back from the transcript),
|
|
the drained entry's id, e.g. ``"pending_a1b2c3"``. Lets a
|
|
client drop the matching optimistic bubble by id instead of
|
|
by position. ``None`` for non-native messages and for messages
|
|
that matched no pending entry (e.g. typed directly in the TUI).
|
|
"""
|
|
|
|
item_id: str
|
|
type: str
|
|
# Heterogeneous payload — concrete shape varies by ``type``
|
|
# (matches :class:`SessionEventInput.data`).
|
|
data: dict[str, Any]
|
|
created_by: str | None = None
|
|
cleared_pending_id: str | None = None
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
|
|
class SessionInputConsumedEvent(_SSEEventBase):
|
|
"""
|
|
A queued input item was materialized into conversation history.
|
|
|
|
Emitted by ``POST /v1/sessions/{id}/events`` once per accepted
|
|
input item at the moment it is persisted into conversation
|
|
history (either onto a steered active turn or as the seed item
|
|
of a freshly-started one). Wire shape uses the NESTED envelope:
|
|
``{"type": "session.input.consumed", "data":
|
|
<:class:`SessionInputConsumedPayload`>, "sequence_number":
|
|
null}``.
|
|
|
|
The event name is **provisional** — it may be renamed in a
|
|
future revision. Consumers should reference
|
|
:data:`SessionInputConsumedEvent` (or its ``type`` literal)
|
|
rather than hardcoding the wire string.
|
|
|
|
:param type: Always ``"session.input.consumed"``.
|
|
:param data: The decoded queued-item payload — see
|
|
:class:`SessionInputConsumedPayload`.
|
|
"""
|
|
|
|
type: Literal["session.input.consumed"]
|
|
data: SessionInputConsumedPayload
|
|
|
|
|
|
class SessionInterruptedPayload(BaseModel):
|
|
"""
|
|
Inner payload of a :class:`SessionInterruptedEvent`.
|
|
|
|
Built by ``_publish_interrupted`` in
|
|
``omnigent/server/routes/sessions.py``.
|
|
|
|
:param requested_at: Unix epoch seconds when the interrupt
|
|
request reached the server, e.g. ``1704067200``.
|
|
:param response_id: Optional active response id for terminal-backed
|
|
integrations, e.g. ``"codex_turn_abc123"``.
|
|
"""
|
|
|
|
requested_at: int
|
|
response_id: str | None = None
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
|
|
class SessionInterruptedEvent(_SSEEventBase):
|
|
"""
|
|
User-triggered cancel reached the loop.
|
|
|
|
Emitted by ``_publish_interrupted`` in
|
|
``omnigent/server/routes/sessions.py`` when a client posts
|
|
a ``{"type": "interrupt"}`` to ``POST
|
|
/v1/sessions/{id}/events``. Co-emitted with
|
|
:class:`IncompleteEvent` (with the underlying response carrying
|
|
``incomplete_details.reason == "user_interrupt"``) so off-the-
|
|
shelf Responses parsers still close cleanly. Wire shape uses
|
|
the NESTED envelope verbatim from the existing emit site.
|
|
|
|
:param type: Always ``"session.interrupted"``.
|
|
:param data: The interrupt metadata — see
|
|
:class:`SessionInterruptedPayload`.
|
|
"""
|
|
|
|
type: Literal["session.interrupted"]
|
|
data: SessionInterruptedPayload
|
|
|
|
|
|
class SessionCreatedEvent(_SSEEventBase):
|
|
"""
|
|
A child (sub-agent) session was spawned from this session.
|
|
|
|
Emitted by ``omnigent/tools/builtins/spawn.py:_spawn_one``
|
|
onto the **parent** session's conversation stream after the
|
|
child conversation row is created and the child task has been
|
|
started. Per the session-rearchitecture spec §3 ("Event types
|
|
and direction") and §7 ("Flow: client interacts with
|
|
sub-agent"), this lets clients watching the parent session's
|
|
SSE subscribe directly to the child's stream without polling
|
|
history for the tunneled ``function_call`` item.
|
|
|
|
The wire shape is FLAT (not enveloped):
|
|
``{"type": "session.created", "conversation_id": <parent>,
|
|
"child_session_id": <child>, "agent_id": <agent or None>,
|
|
"parent_session_id": <parent>, "sequence_number": null}``.
|
|
|
|
The existing tunneled ``function_call`` ConversationItem
|
|
(carried inside :class:`OutputItemDoneEvent`) is retained
|
|
for compatibility — clients that don't yet implement the
|
|
"subscribe to child stream" pattern can keep rendering sub-
|
|
agent calls from the parent's persistent history.
|
|
|
|
:param type: Always ``"session.created"``.
|
|
:param conversation_id: The PARENT session/conversation id —
|
|
this event rides the parent's stream, e.g.
|
|
``"conv_parent123"``.
|
|
:param child_session_id: The newly-created child session id,
|
|
e.g. ``"conv_child456"``. Same as ``conversation_id`` on
|
|
the child's own stream when consumers pivot to it.
|
|
:param agent_id: Registered agent id the child runs as,
|
|
e.g. ``"agent_xyz"``. ``None`` is permitted only for
|
|
legacy spawn paths that did not record an agent id;
|
|
new code MUST set it.
|
|
:param parent_session_id: Echo of ``conversation_id`` for
|
|
consumers that key on a dedicated "parent" field rather
|
|
than the carrier ``conversation_id``. Always equal to
|
|
``conversation_id``; included for forward-compat with
|
|
clients that may relay these events across stream
|
|
boundaries.
|
|
|
|
Category: **transient** (SSE-only). The corresponding durable
|
|
record of "a child session exists" lives in the conversation
|
|
store as the child conversation row itself
|
|
(``parent_conversation_id`` foreign key) and the parent's
|
|
tunneled ``function_call`` item — reconnecting clients
|
|
discover children by walking the parent's persisted history,
|
|
not by replaying this event.
|
|
"""
|
|
|
|
type: Literal["session.created"]
|
|
conversation_id: str
|
|
child_session_id: str
|
|
agent_id: str | None = None
|
|
parent_session_id: str | None = None
|
|
|
|
|
|
class SessionSupersededEvent(_SSEEventBase):
|
|
"""
|
|
This conversation was superseded by another and clients should
|
|
follow to it.
|
|
|
|
Emitted by ``_publish_session_superseded`` in
|
|
``omnigent/server/routes/sessions.py`` when the claude-native
|
|
forwarder rotates a session away on a Claude ``/clear`` (the old
|
|
conversation keeps its history but the live terminal moves to a
|
|
fresh conversation — see ``_post_clear_supersession`` in
|
|
``omnigent/claude_native_forwarder.py``). A client actively viewing
|
|
the superseded conversation auto-redirects to ``target_conversation_id``.
|
|
|
|
Category: **transient** (SSE-only), live-only by design. There is no
|
|
SSE replay: a client that connects after the rotation does not get
|
|
this event. The durable counterpart is the persisted notice message
|
|
appended to the old conversation (a ``message`` item linking to the
|
|
new conversation), which a reloading client renders instead of being
|
|
force-redirected.
|
|
|
|
The wire shape is FLAT (not enveloped):
|
|
``{"type": "session.superseded", "conversation_id": <old>,
|
|
"target_conversation_id": <new>, "reason": "clear"}``.
|
|
|
|
:param type: Always ``"session.superseded"``.
|
|
:param conversation_id: The superseded (old) conversation id this
|
|
event rides the stream of, e.g. ``"conv_old"``.
|
|
:param target_conversation_id: The conversation to follow to, e.g.
|
|
``"conv_new"``.
|
|
:param reason: Why the session was superseded. Currently always
|
|
``"clear"`` (a Claude Code ``/clear``); kept as a field so the
|
|
client can branch on future supersession causes.
|
|
"""
|
|
|
|
type: Literal["session.superseded"]
|
|
conversation_id: str
|
|
target_conversation_id: str
|
|
reason: Literal["clear"] = "clear"
|
|
|
|
|
|
# ── Response pass-through events (response.*) ──────────────────────
|
|
|
|
|
|
class OutputTextDeltaEvent(_SSEEventBase):
|
|
"""
|
|
Incremental assistant-text token emitted during streaming.
|
|
|
|
Wire shape matches the existing raw-dict emit at
|
|
``omnigent/runtime/workflow.py:1352-1356``.
|
|
|
|
:param type: Always ``"response.output_text.delta"``.
|
|
:param delta: The text fragment for this chunk, e.g.
|
|
``"Hello"``.
|
|
:param message_id: For terminal-observed streaming (claude-native),
|
|
the vendor's stable per-message id, e.g.
|
|
``"2ca51d97-2f0f-493a-aed7-85a5b56c5747"``. Lets the web UI scope
|
|
an in-flight buffer to one assistant message and reconcile it
|
|
against the final item. ``None`` for ordinary in-process task
|
|
streaming, where deltas already group by the active response.
|
|
:param index: 0-based chunk order within the message, e.g. ``3``.
|
|
``None`` when not terminal-observed streaming.
|
|
:param final: ``True`` on the last chunk of a terminal-observed
|
|
message; ``None`` otherwise. Signals the web UI that no further
|
|
chunks for ``message_id`` will arrive.
|
|
"""
|
|
|
|
type: Literal["response.output_text.delta"]
|
|
delta: str
|
|
message_id: str | None = None
|
|
index: int | None = None
|
|
final: bool | None = None
|
|
|
|
|
|
class ReasoningStartedEvent(_SSEEventBase):
|
|
"""
|
|
Marker emitted once when a reasoning block begins.
|
|
|
|
Fired even when the reasoning content itself is encrypted /
|
|
redacted (so no delta events follow), letting clients render
|
|
a "thinking…" indicator regardless of provider verification
|
|
status. Wire shape matches ``omnigent/runtime/workflow.py:1350``.
|
|
|
|
:param type: Always ``"response.reasoning.started"``.
|
|
"""
|
|
|
|
type: Literal["response.reasoning.started"]
|
|
|
|
|
|
class ReasoningTextDeltaEvent(_SSEEventBase):
|
|
"""
|
|
Incremental reasoning-text token (full chain-of-thought).
|
|
|
|
Only emitted by providers that surface reasoning content
|
|
(e.g. OpenAI o-series with appropriate verification). Wire
|
|
shape matches ``omnigent/runtime/workflow.py:1358-1364``.
|
|
|
|
:param type: Always ``"response.reasoning_text.delta"``.
|
|
:param delta: The reasoning text fragment, e.g.
|
|
``"Considering the user's intent..."``.
|
|
"""
|
|
|
|
type: Literal["response.reasoning_text.delta"]
|
|
delta: str
|
|
|
|
|
|
class ReasoningSummaryTextDeltaEvent(_SSEEventBase):
|
|
"""
|
|
Incremental reasoning-summary token.
|
|
|
|
Emitted when ``reasoning.summary`` is configured on the
|
|
request. Wire shape matches
|
|
``omnigent/runtime/workflow.py:1370-1373``.
|
|
|
|
:param type: Always ``"response.reasoning_summary_text.delta"``.
|
|
:param delta: The summary text fragment, e.g. ``"Will use
|
|
the search tool to gather context."``.
|
|
"""
|
|
|
|
type: Literal["response.reasoning_summary_text.delta"]
|
|
delta: str
|
|
|
|
|
|
class OutputItemDoneEvent(_SSEEventBase):
|
|
"""
|
|
A conversation output item completed during the turn.
|
|
|
|
Carries any item type the conversation persists (message,
|
|
function_call, function_call_output, reasoning, compaction,
|
|
native_tool, …). The ``item`` payload's wire shape merges
|
|
common fields (``id``, ``type``, ``status``) with the
|
|
type-specific data fields — it is NOT nested as
|
|
``{type, data}``.
|
|
|
|
:param type: Always ``"response.output_item.done"``.
|
|
:param item: The completed item dict. Heterogeneous and
|
|
item-type-specific; see
|
|
``omnigent/entities/conversation.py`` for the
|
|
per-type ``*Data`` shapes that drive serialization.
|
|
Example for a function_call item: ``{"id": "fc_abc123",
|
|
"type": "function_call", "status": "action_required",
|
|
"name": "search.web", "arguments": "{\\"q\\": \\"foo\\"}",
|
|
"call_id": "call_xyz"}``.
|
|
"""
|
|
|
|
type: Literal["response.output_item.done"]
|
|
# ``dict[str, Any]`` because items are heterogeneous and
|
|
# type-specific (their per-type ``*Data`` shapes already
|
|
# live in entities/conversation.py via ITEM_TYPE_TO_DATA_CLS).
|
|
# Modeling each variant here would duplicate that mapping;
|
|
# consumers that need typed item data parse via
|
|
# ``parse_item_data(item["type"], item)``.
|
|
item: dict[str, Any]
|
|
|
|
|
|
class InjectionConsumedEvent(_SSEEventBase):
|
|
"""
|
|
Runner-internal marker: a mid-turn injection was consumed.
|
|
|
|
Emitted by the executor adapter (``_watch_injections``) once the
|
|
inner executor accepts a live mid-turn injection into the running
|
|
turn. It rides the harness→runner turn stream and is intercepted by
|
|
the runner's proxy_stream relay: the runner drops the buffered copy
|
|
of the matching message so it is NOT re-delivered as a continuation
|
|
turn (RUNNER_MESSAGE_INGEST.md Part B). This event is **never**
|
|
published to the client session stream or relayed upstream — it is
|
|
purely a runner-internal exactly-once handshake.
|
|
|
|
:param type: Always ``"injection.consumed"``.
|
|
:param injection_id: Correlation id the runner stamped on the
|
|
forwarded injection, e.g. ``"inj_ab12cd34ef56"``. Matches the
|
|
``injection_id`` on the buffered message the runner drops.
|
|
"""
|
|
|
|
type: Literal["injection.consumed"]
|
|
injection_id: str
|
|
|
|
|
|
class OutputFileDoneEvent(_SSEEventBase):
|
|
"""
|
|
A streamed file output completed materializing.
|
|
|
|
Emitted by ``_emit_file_annotation_events`` in
|
|
``omnigent/runtime/workflow.py`` once per file annotation in
|
|
the assistant's output. ``filename`` and ``content_type`` are
|
|
only populated when the originating annotation carried them.
|
|
|
|
:param type: Always ``"response.output_file.done"``.
|
|
:param file_id: Identifier of the materialized file,
|
|
e.g. ``"file_abc123"``.
|
|
:param filename: Original filename if the annotation supplied
|
|
one, e.g. ``"report.pdf"``. ``None`` otherwise.
|
|
:param content_type: MIME content type if the annotation
|
|
supplied one, e.g. ``"application/pdf"``. ``None``
|
|
otherwise.
|
|
"""
|
|
|
|
type: Literal["response.output_file.done"]
|
|
file_id: str
|
|
filename: str | None = None
|
|
content_type: str | None = None
|
|
|
|
|
|
class HeartbeatEvent(_SSEEventBase):
|
|
"""
|
|
Keepalive event emitted on a fixed cadence during streaming.
|
|
|
|
Lets consumers detect stalled producers via missed-interval
|
|
timing. Cadence is set by ``_HEARTBEAT_INTERVAL_S`` in
|
|
``omnigent/runtime/workflow.py`` (15 seconds at the time of
|
|
writing). Wire shape matches the existing emit at
|
|
``omnigent/runtime/workflow.py:4636-4639``.
|
|
|
|
Per ``designs/SERVER_HARNESS_CONTRACT.md`` §Heartbeats, the
|
|
event MAY carry timing metadata so consumers can do richer
|
|
dead-detection than "did anything arrive":
|
|
|
|
- ``server_time`` is the producer's wall-clock at emission,
|
|
letting consumers detect clock drift between producer and
|
|
consumer.
|
|
- ``last_event_seq`` is the ``sequence_number`` of the most
|
|
recent NON-heartbeat event (or ``None`` when this is the
|
|
first heartbeat before any user-visible event), letting
|
|
consumers detect dropped events on reconnect.
|
|
|
|
Both fields are optional on the wire (``None`` round-trips as
|
|
omitted) so older AP→harness pairs that pre-date the field
|
|
addition still parse cleanly.
|
|
|
|
:param type: Always ``"response.heartbeat"``.
|
|
:param server_time: ISO 8601 UTC timestamp at emission, e.g.
|
|
``"2026-04-27T15:30:00Z"``. ``None`` when the producer
|
|
chose not to populate it (legacy emitters).
|
|
:param last_event_seq: Sequence number of the last non-
|
|
heartbeat event seen on the same stream, e.g. ``42``.
|
|
``None`` before any user-visible event has fired (first
|
|
heartbeat of the turn, before deltas land), or when the
|
|
producer chose not to populate it.
|
|
"""
|
|
|
|
type: Literal["response.heartbeat"]
|
|
server_time: str | None = None
|
|
last_event_seq: int | None = None
|
|
|
|
|
|
class SessionHeartbeatEvent(_SSEEventBase):
|
|
"""
|
|
Idle-stream keepalive on ``GET /v1/sessions/{id}/stream``.
|
|
|
|
Emitted by the session-stream route on a fixed cadence whenever
|
|
the underlying publish queue has been quiet (no turn in flight,
|
|
no resource events). Distinct from :class:`HeartbeatEvent`
|
|
(``response.heartbeat``), which is per-turn and is driven by
|
|
the runtime workflow while a response is producing output.
|
|
|
|
Why this exists: the session stream stays open across many turns
|
|
and through idle periods (waiting for the user to type). Without
|
|
a periodic emit, intermediate proxies, OS-level sockets, and the
|
|
client's SSE read-timeout can leave a half-open stream
|
|
undetected for minutes after a network event (laptop sleep,
|
|
Wi-Fi handoff). The heartbeat puts a regular byte on the wire
|
|
so the client's read-timeout and the server's
|
|
``request.is_disconnected()`` check both fire promptly.
|
|
|
|
Consumers MAY ignore the payload entirely (the bytes crossing
|
|
the wire are sufficient). The optional ``server_time`` mirrors
|
|
:class:`HeartbeatEvent` for symmetry and debugging.
|
|
|
|
:param type: Always ``"session.heartbeat"``.
|
|
:param server_time: ISO 8601 UTC timestamp at emission, e.g.
|
|
``"2026-05-25T10:30:00Z"``. ``None`` when the producer
|
|
chose not to populate it.
|
|
"""
|
|
|
|
type: Literal["session.heartbeat"]
|
|
server_time: str | None = None
|
|
|
|
|
|
class PresenceViewer(BaseModel):
|
|
"""
|
|
One user currently viewing a session (holding its SSE stream open).
|
|
|
|
:param user_id: The viewer's authenticated identity,
|
|
e.g. ``"alice@example.com"``. Never the reserved single-user
|
|
``"local"`` sentinel — presence only tracks distinct human
|
|
actors (see ``attribution_user``).
|
|
:param joined_at: ISO 8601 UTC timestamp of when the user joined,
|
|
e.g. ``"2026-06-10T17:00:00Z"``. Stable across reconnects
|
|
within the server's leave-grace window.
|
|
:param idle: Whether every stream the user holds reports an idle
|
|
(backgrounded) tab. The web greys idle viewers' avatars.
|
|
"""
|
|
|
|
user_id: str
|
|
joined_at: str
|
|
idle: bool = False
|
|
|
|
|
|
class SessionPresenceEvent(_SSEEventBase):
|
|
"""
|
|
The session's viewer list changed — full state, not a delta.
|
|
|
|
Emitted on ``GET /v1/sessions/{id}/stream`` whenever a user
|
|
joins, leaves (after the server-side grace window absorbs
|
|
reconnect churn), or flips their idle aggregate, and once to
|
|
each newly-connected stream as a snapshot-on-connect. Every
|
|
event carries the COMPLETE viewer list so clients replace their
|
|
state wholesale — missed events self-heal on the next event or
|
|
reconnect. Viewers are scoped to the session *tree* (the root
|
|
conversation and every sub-agent conversation under it), so a
|
|
user on a sub-agent page and a user on the root page appear in
|
|
each other's lists. See ``omnigent/server/presence.py`` and
|
|
``designs/UI/PRESENCE.md``.
|
|
|
|
:param type: Always ``"session.presence"``.
|
|
:param conversation_id: The conversation whose stream delivered
|
|
this event — the root or a sub-agent conversation, e.g.
|
|
``"conv_abc123"``. Matches the streamed conversation (not
|
|
necessarily the tree's root) so clients can guard events by
|
|
the conversation they are viewing.
|
|
:param viewers: All users currently viewing any conversation in
|
|
the session tree (including the receiving user — the web
|
|
filters self out for display), ordered by join time.
|
|
"""
|
|
|
|
type: Literal["session.presence"]
|
|
conversation_id: str
|
|
viewers: list[PresenceViewer]
|
|
|
|
|
|
class ElicitationRequestParams(BaseModel):
|
|
"""
|
|
Inner ``params`` block of a :class:`ElicitationRequestEvent`.
|
|
|
|
The standard fields (``mode``, ``message``, ``requestedSchema``,
|
|
``url``) mirror MCP's ``ElicitRequestFormParams`` /
|
|
``ElicitRequestUrlParams`` byte-for-byte (Principle 8 — adopt
|
|
MCP's wire shape verbatim where it overlaps). The
|
|
AP-specific extensions (``phase``, ``policy_name``,
|
|
``content_preview``, ``target_session_id``) carry policy-engine
|
|
context and mirrored-child routing for the consumer's renderer;
|
|
MCP's ``extra="allow"`` config permits them under the same params
|
|
block. Wire shape matches
|
|
``omnigent/runtime/policies/approval.py:175``.
|
|
|
|
:param mode: MCP-standard discriminator. ``"form"`` collects
|
|
structured input via ``requestedSchema``; ``"url"``
|
|
directs upstream to an external URL for OAuth /
|
|
out-of-band interaction.
|
|
:param message: Human-readable prompt the consumer renders,
|
|
e.g. ``"Approve running 'rm -rf /tmp/cache'?"``.
|
|
:param requestedSchema: JSON-Schema dict for form mode (or
|
|
``None`` for url mode). camelCase preserved per MCP
|
|
spec, e.g.
|
|
``{"type": "object", "properties": {"approve":
|
|
{"type": "boolean"}}}``.
|
|
:param url: External URL for url mode (or ``None`` for form
|
|
mode), e.g. ``"https://oauth.example.com/authorize?..."``.
|
|
:param phase: Omnigent policy-engine phase the elicitation
|
|
belongs to, e.g. ``"pre_tool_use"``.
|
|
:param policy_name: Omnigent policy that triggered the
|
|
elicitation, e.g. ``"approve_shell_commands"``.
|
|
:param content_preview: Truncated preview of the underlying
|
|
request payload (≤1024 chars in current AP), for the
|
|
consumer's renderer.
|
|
:param target_session_id: AP session whose resolve endpoint owns
|
|
this elicitation, e.g. ``"conv_child123"``. Present when a
|
|
child/sub-agent prompt is mirrored into an ancestor stream;
|
|
``None`` means resolve against the current session.
|
|
"""
|
|
|
|
mode: Literal["form", "url"] = "form"
|
|
message: str
|
|
requestedSchema: dict[str, Any] | None = None
|
|
url: str | None = None
|
|
# AP-specific extensions — allowed under MCP's
|
|
# ``extra="allow"`` policy on the inner params object. Strict
|
|
# MCP clients ignore unknown fields here.
|
|
phase: str | None = None
|
|
policy_name: str | None = None
|
|
content_preview: str | None = None
|
|
target_session_id: str | None = None
|
|
|
|
# MCP's ElicitRequestParams uses ``extra="allow"``; mirror
|
|
# that here so MCP-shaped passthrough (an MCP server's
|
|
# ``elicitation/create`` traversing harness → Omnigent → client)
|
|
# preserves any fields the MCP server added.
|
|
model_config = ConfigDict(extra="allow")
|
|
|
|
|
|
class ElicitationRequestEvent(_SSEEventBase):
|
|
"""
|
|
Synchronous request for a decision from upstream.
|
|
|
|
Emitted by Omnigent (or, under the new contract, by a harness)
|
|
when the LLM / a tool / a policy needs a verdict before
|
|
proceeding. The consumer replies via
|
|
``POST /v1/sessions/{session_id}/events`` with
|
|
``type == "approval"`` and
|
|
:class:`omnigent.server.schemas.ElicitationResult` fields in
|
|
``data``. This preserves MCP request/reply correlation by id
|
|
without threading elicitations through PATCH.
|
|
|
|
Wire shape matches the existing emit at
|
|
``omnigent/runtime/policies/approval.py:175``.
|
|
|
|
:param type: Always ``"response.elicitation_request"``.
|
|
:param elicitation_id: Unique correlation id for this
|
|
request — appears in the consumer's approval event payload,
|
|
e.g. ``"elicit_abc123"``.
|
|
:param method: MCP method literal — always
|
|
``"elicitation/create"`` (the value of
|
|
``_MCP_ELICITATION_METHOD`` in
|
|
``omnigent/runtime/policies/approval.py``).
|
|
:param params: The MCP-shaped params block carrying the
|
|
prompt and (form-mode only) the requested schema.
|
|
"""
|
|
|
|
type: Literal["response.elicitation_request"]
|
|
elicitation_id: str
|
|
# MCP method constant — kept as Literal so the discriminator
|
|
# accepts only the MCP-standard value; harnesses that emit a
|
|
# different method literal will fail validation loudly.
|
|
method: Literal["elicitation/create"] = "elicitation/create"
|
|
params: ElicitationRequestParams
|
|
|
|
|
|
class BrowserActionRequestEvent(_SSEEventBase):
|
|
"""
|
|
Request that the desktop renderer perform one browser action.
|
|
|
|
Emitted by the server ``POST /v1/sessions/{id}/browser/action_request``
|
|
route when a runner-side ``browser_*`` tool dispatch needs the
|
|
Omnigent desktop app's embedded browser to act. The event fans out
|
|
on the session stream to every subscribed renderer; each renderer
|
|
first POSTs ``/browser/action_claim/{action_id}`` and only the
|
|
winning claimant executes the action and POSTs the result back to
|
|
``/browser/action_result/{action_id}``. The claim lease prevents
|
|
double execution when more than one renderer is subscribed.
|
|
|
|
:param type: Always ``"browser.action_request"``.
|
|
:param action_id: Unique correlation id for this request, e.g.
|
|
``"baction_abc123"``. Echoed on the claim and result routes.
|
|
:param action: The browser action to perform — the ``browser_``
|
|
tool name with the prefix stripped, e.g. ``"navigate"``,
|
|
``"snapshot"``, ``"click"``, ``"type"``, ``"screenshot"``.
|
|
:param args: Action arguments forwarded from the tool call, e.g.
|
|
``{"url": "https://example.com"}``.
|
|
"""
|
|
|
|
type: Literal["browser.action_request"]
|
|
action_id: str
|
|
action: str
|
|
args: dict[str, Any]
|
|
|
|
|
|
class ElicitationResolvedEvent(_SSEEventBase):
|
|
"""
|
|
Signal that a previously-published elicitation is no longer
|
|
outstanding, even though no UI ``approval`` verdict was
|
|
delivered through ``POST /v1/sessions/{id}/events``.
|
|
|
|
Emitted by the runner when its own ``_pending_approvals``
|
|
Future is popped without a verdict (the runner's wait timed
|
|
out, the turn was cancelled, the harness exited) so the AP
|
|
server's :mod:`omnigent.runtime.pending_elicitations`
|
|
index can decrement the sidebar badge in lockstep with the
|
|
underlying awaiter's lifecycle. Without this signal, the AP
|
|
server has no way to learn that the prompt is dead and the
|
|
badge stays stuck.
|
|
|
|
Idempotent on the consumer side: the Omnigent server's index
|
|
decrement is a no-op when the id isn't tracked, so the
|
|
runner can fire-and-forget on every Future cleanup.
|
|
|
|
:param type: Always ``"response.elicitation_resolved"``.
|
|
:param elicitation_id: Correlation id of the elicitation
|
|
being cleared, e.g. ``"elicit_abc123"``. Must match the
|
|
id of a prior :class:`ElicitationRequestEvent`.
|
|
"""
|
|
|
|
type: Literal["response.elicitation_resolved"]
|
|
elicitation_id: str
|
|
|
|
|
|
class PolicyDeniedEvent(_SSEEventBase):
|
|
"""
|
|
Signal that a policy DENY was enforced on a native harness turn.
|
|
|
|
A native harness (Claude Code, Codex, ...) routes each tool call and
|
|
prompt through Omnigent's policy engine via the vendor command-hook
|
|
(``POST /v1/sessions/{id}/policies/evaluate``). The DENY verdict is
|
|
returned synchronously to that hook, so unlike the SDK/wrap path there is
|
|
no stream-visible signal that a native action was blocked — only the
|
|
*effect* (the blocked tool never runs). This event surfaces the decision
|
|
itself on the session stream so observers (the web UI, the capability
|
|
bench) can see a native DENY as a positive signal rather than infer it
|
|
from an absence.
|
|
|
|
Fire-and-forget and observational: it does not gate the turn (the hook
|
|
response already did that) and carries no correlation id.
|
|
|
|
:param type: Always ``"response.policy_denied"``.
|
|
:param conversation_id: Session/conversation id the DENY applies to,
|
|
e.g. ``"conv_abc123"``.
|
|
:param reason: Human-readable deny reason from the deciding policy, e.g.
|
|
``"Blocked by policy."``.
|
|
:param phase: The policy phase the DENY landed on, e.g. ``"tool_call"``.
|
|
"""
|
|
|
|
type: Literal["response.policy_denied"]
|
|
conversation_id: str
|
|
reason: str = ""
|
|
phase: str = ""
|
|
|
|
|
|
class CreatedEvent(_SSEEventBase):
|
|
"""
|
|
Initial event emitted at the start of every streaming response.
|
|
|
|
Carries the freshly-allocated
|
|
:class:`omnigent.server.schemas.ResponseObject` (status will
|
|
be ``"queued"`` or ``"in_progress"`` depending on whether the
|
|
task started immediately).
|
|
|
|
:param type: Always ``"response.created"``.
|
|
:param response: The newly-allocated response object.
|
|
"""
|
|
|
|
type: Literal["response.created"]
|
|
response: ResponseObject
|
|
|
|
|
|
class QueuedEvent(_SSEEventBase):
|
|
"""
|
|
Optional event emitted between ``created`` and ``in_progress``
|
|
for background tasks that are queued before they start.
|
|
|
|
Foreground streaming responses skip this event.
|
|
|
|
:param type: Always ``"response.queued"``.
|
|
:param response: The response object with
|
|
``status="queued"``.
|
|
"""
|
|
|
|
type: Literal["response.queued"]
|
|
response: ResponseObject
|
|
|
|
|
|
class InProgressEvent(_SSEEventBase):
|
|
"""
|
|
Event emitted once the task transitions to in-progress.
|
|
|
|
Always follows ``response.created`` (and ``response.queued``
|
|
for background tasks).
|
|
|
|
:param type: Always ``"response.in_progress"``.
|
|
:param response: The response object with
|
|
``status="in_progress"``.
|
|
"""
|
|
|
|
type: Literal["response.in_progress"]
|
|
response: ResponseObject
|
|
|
|
|
|
class CompletedEvent(_SSEEventBase):
|
|
"""
|
|
Terminal event for a successfully completed turn.
|
|
|
|
Carries the final
|
|
:class:`omnigent.server.schemas.ResponseObject`.
|
|
|
|
:param type: Always ``"response.completed"``.
|
|
:param response: The final response object with
|
|
``status="completed"``.
|
|
"""
|
|
|
|
type: Literal["response.completed"]
|
|
response: ResponseObject
|
|
|
|
|
|
class FailedEvent(_SSEEventBase):
|
|
"""
|
|
Terminal event for a turn that ended with an error.
|
|
|
|
Carries the final
|
|
:class:`omnigent.server.schemas.ResponseObject` whose
|
|
``error`` field describes the failure.
|
|
|
|
:param type: Always ``"response.failed"``.
|
|
:param response: The final response object with
|
|
``status="failed"`` and ``error`` populated.
|
|
"""
|
|
|
|
type: Literal["response.failed"]
|
|
response: ResponseObject
|
|
|
|
|
|
class CancelledEvent(_SSEEventBase):
|
|
"""
|
|
Terminal event for a turn cancelled before completion.
|
|
|
|
:param type: Always ``"response.cancelled"``.
|
|
:param response: The final response object with
|
|
``status="cancelled"``.
|
|
"""
|
|
|
|
type: Literal["response.cancelled"]
|
|
response: ResponseObject
|
|
|
|
|
|
class IncompleteEvent(_SSEEventBase):
|
|
"""
|
|
Terminal event for a turn that ended without completing
|
|
(e.g. hit the iteration cap or token budget).
|
|
|
|
:param type: Always ``"response.incomplete"``.
|
|
:param response: The final response object with
|
|
``status="incomplete"`` and ``incomplete_details``
|
|
populated describing the reason.
|
|
"""
|
|
|
|
type: Literal["response.incomplete"]
|
|
response: ResponseObject
|
|
|
|
|
|
class RetryErrorDetail(BaseModel):
|
|
"""
|
|
Error block carried by :class:`RetryEvent` and :class:`ErrorEvent`.
|
|
|
|
Mirrors the shape that ``llm_retry.py`` and ``tool_retry.py``
|
|
emit today — flat ``code`` / ``message`` plus an optional
|
|
``detail`` for provider-specific structured fields.
|
|
|
|
:param code: Stable error classifier, e.g. ``"timeout"``,
|
|
``"rate_limit"``.
|
|
:param message: Human-readable summary, e.g.
|
|
``"Connection timed out after 30s"``.
|
|
:param detail: Optional provider-specific structured fields
|
|
(e.g. ``{"status_code": 429, "retry_after": 5}``);
|
|
``None`` when the classifier had no extra context.
|
|
"""
|
|
|
|
code: str
|
|
message: str
|
|
detail: dict[str, Any] | None = None
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
|
|
class RetryEvent(_SSEEventBase):
|
|
"""
|
|
A retryable failure was caught and a retry is scheduled.
|
|
|
|
Emitted by ``omnigent/runtime/llm_retry.py`` (LLM calls)
|
|
and ``omnigent/runtime/tool_retry.py`` (tool calls) before
|
|
sleeping for the backoff delay. Wire shape matches
|
|
``llm_retry.py:329-340`` and ``tool_retry.py:168-180``.
|
|
|
|
:param type: Always ``"response.retry"``.
|
|
:param source: Origin of the retried failure — ``"llm"`` for
|
|
LLM-call retries, ``"tool"`` for tool-call retries.
|
|
:param tool_name: Tool identifier when ``source == "tool"``,
|
|
e.g. ``"search.web"``. ``None`` for LLM retries.
|
|
:param attempt: 1-based count of the upcoming attempt
|
|
(i.e. attempt that will run AFTER this delay), e.g.
|
|
``2`` for the first retry.
|
|
:param max_attempts: Total tries allowed by the retry policy,
|
|
e.g. ``3``. Lets clients render "attempt 2 of 3".
|
|
:param delay_seconds: Seconds the producer will sleep before
|
|
retrying, rounded to two decimals, e.g. ``1.5``.
|
|
:param error: Classified error description for the failure
|
|
being retried.
|
|
"""
|
|
|
|
type: Literal["response.retry"]
|
|
source: Literal["llm", "tool"]
|
|
tool_name: str | None = None
|
|
attempt: int
|
|
max_attempts: int
|
|
delay_seconds: float
|
|
error: RetryErrorDetail
|
|
|
|
|
|
class ErrorEvent(_SSEEventBase):
|
|
"""
|
|
Non-recoverable error reported during the turn.
|
|
|
|
Emitted from multiple sites in
|
|
``omnigent/runtime/workflow.py`` — terminal LLM failures
|
|
(``_emit_llm_error_event``), execution timeouts
|
|
(``_handle_execution_timeout``), and the agent-loop catch-all
|
|
(``except Exception``). Wire shape matches those emits.
|
|
|
|
:param type: Always ``"response.error"``.
|
|
:param source: Origin of the error — ``"llm"`` for LLM-call
|
|
failures, ``"execution"`` for timeouts, ``"tool"`` for
|
|
tool failures (currently emitted by retry exhaustion paths).
|
|
:param tool_name: Tool identifier when ``source == "tool"``;
|
|
``None`` for the other sources.
|
|
:param error: Classified error description.
|
|
"""
|
|
|
|
type: Literal["response.error"]
|
|
source: Literal["llm", "execution", "tool"]
|
|
tool_name: str | None = None
|
|
error: RetryErrorDetail
|
|
|
|
|
|
class CompactionInProgressEvent(_SSEEventBase):
|
|
"""
|
|
Conversation history is being compacted.
|
|
|
|
Emitted by ``omnigent/runtime/compaction.py`` while a
|
|
compaction step runs so clients can render a "summarizing
|
|
history…" indicator. Wire shape matches ``compaction.py:765``.
|
|
|
|
:param type: Always ``"response.compaction.in_progress"``.
|
|
"""
|
|
|
|
type: Literal["response.compaction.in_progress"]
|
|
|
|
|
|
class CompactionCompletedEvent(_SSEEventBase):
|
|
"""
|
|
Conversation history compaction has finished.
|
|
|
|
Emitted after compaction completes — either by the server after
|
|
``compact_conversation_now()`` (explicit ``/compact``), or by a
|
|
harness that compacted its own internal context. Clients that
|
|
rendered a "Compacting…" spinner on
|
|
:class:`CompactionInProgressEvent` should upgrade it to the
|
|
permanent "Conversation compacted" marker on this event.
|
|
|
|
When emitted by a harness, ``summary`` and ``summary_model``
|
|
are populated so the runner can persist a compaction item for
|
|
session resume. When emitted by the server's explicit
|
|
``/compact`` path, those fields are ``None``.
|
|
|
|
:param type: Always ``"response.compaction.completed"``.
|
|
:param total_tokens: Tiktoken estimate of the post-compaction
|
|
message context size, e.g. ``8421``. Used by clients to
|
|
update the context-ring immediately without waiting for the
|
|
next ``response.completed`` usage report. ``None`` when
|
|
token counting is unavailable.
|
|
:param summary: Text summary of the compacted conversation,
|
|
or ``None`` for server-side compaction (already persisted).
|
|
:param summary_model: Model used for summarization, or ``None``
|
|
if truncation-based or server-side.
|
|
"""
|
|
|
|
type: Literal["response.compaction.completed"]
|
|
total_tokens: int | None = None
|
|
summary: str | None = None
|
|
summary_model: str | None = None
|
|
compacted_messages: list[dict[str, Any]] | None = None
|
|
|
|
|
|
class CompactionFailedEvent(_SSEEventBase):
|
|
"""
|
|
Conversation history compaction failed.
|
|
|
|
Emitted by ``omnigent/server/routes/sessions.py`` when
|
|
``compact_conversation_now()`` raises. Clients that rendered a
|
|
"Compacting…" spinner on :class:`CompactionInProgressEvent`
|
|
should dismiss it without leaving a permanent marker, since the
|
|
conversation history was not modified.
|
|
|
|
:param type: Always ``"response.compaction.failed"``.
|
|
"""
|
|
|
|
type: Literal["response.compaction.failed"]
|
|
|
|
|
|
class ClientTaskCancelEvent(_SSEEventBase):
|
|
"""
|
|
Server-side request that the client cancel a tunneled tool call.
|
|
|
|
Emitted by ``omnigent/runtime/workflow.py`` when a parent
|
|
cancellation needs to propagate to a long-running async client
|
|
tool. Wire shape matches ``workflow.py:4258-4266``.
|
|
|
|
:param type: Always ``"response.client_task.cancel"``.
|
|
:param task_id: Identifier of the client-side task being
|
|
cancelled, e.g. ``"resp_async_abc"``.
|
|
:param call_id: Synthetic ``call_id`` the SDK uses to
|
|
reconcile the local task; ``None`` when no pending tool
|
|
call row exists for the task.
|
|
"""
|
|
|
|
type: Literal["response.client_task.cancel"]
|
|
task_id: str
|
|
call_id: str | None = None
|
|
|
|
|
|
# ── Session resource lifecycle events (Phase 1d) ─────────────────────
|
|
|
|
|
|
class SessionResourceCreatedEvent(_SSEEventBase):
|
|
"""
|
|
A session resource was created.
|
|
|
|
Emitted when a terminal is launched, a file is uploaded, or
|
|
any other resource is materialized under a session. Wire shape
|
|
is FLAT: ``{"type": "session.resource.created",
|
|
"resource": <SessionResourceObject-like dict>}``.
|
|
|
|
:param type: Always ``"session.resource.created"``.
|
|
:param resource: The newly created resource object.
|
|
"""
|
|
|
|
type: Literal["session.resource.created"]
|
|
resource: dict[str, Any]
|
|
|
|
|
|
class SessionResourceDeletedEvent(_SSEEventBase):
|
|
"""
|
|
A session resource was deleted.
|
|
|
|
Emitted when a terminal is closed, a file is deleted, or
|
|
any other resource is removed from a session.
|
|
|
|
:param type: Always ``"session.resource.deleted"``.
|
|
:param resource_id: Opaque id of the deleted resource.
|
|
:param resource_type: Type of the deleted resource,
|
|
e.g. ``"terminal"``, ``"file"``.
|
|
:param session_id: Owning session/conversation id.
|
|
"""
|
|
|
|
type: Literal["session.resource.deleted"]
|
|
resource_id: str
|
|
resource_type: str
|
|
session_id: str
|
|
|
|
|
|
class SessionChildSessionUpdatedEvent(_SSEEventBase):
|
|
"""
|
|
A child (sub-agent) session's status changed — pushed to the PARENT.
|
|
|
|
Lets the parent's resource rail update a child's status without
|
|
polling ``GET …/child_sessions``. Carries the full
|
|
:class:`ChildSessionSummary` so the web patches its cache directly.
|
|
|
|
:param type: Always ``"session.child_session.updated"``.
|
|
:param conversation_id: The PARENT (carrier) session id.
|
|
:param child_session_id: The child session id, e.g.
|
|
``"conv_child_abc123"``.
|
|
:param child: A PARTIAL :class:`ChildSessionSummary` — the
|
|
snapshot-on-connect sends the full summary, while live runner
|
|
deltas carry only the fields that changed (a status delta omits
|
|
``last_message_preview``; a preview delta carries only it). The
|
|
web merges present fields over the cached row, so the payload is
|
|
an open dict rather than the strict model.
|
|
"""
|
|
|
|
type: Literal["session.child_session.updated"]
|
|
conversation_id: str
|
|
child_session_id: str
|
|
child: dict[str, Any]
|
|
|
|
|
|
class SessionChangedFilesInvalidatedEvent(_SSEEventBase):
|
|
"""
|
|
The session's changed-files list may have changed — refetch it.
|
|
|
|
A coarse "something changed" signal (per-file events aren't available
|
|
for git-mode workspaces) emitted by the runner after a file-mutating
|
|
tool. The web treats it as a refetch trigger for the changed-files
|
|
panel; transient (not persisted — the REST list is source of truth).
|
|
|
|
:param type: Always ``"session.changed_files.invalidated"``.
|
|
:param session_id: Owning session/conversation id.
|
|
:param environment_id: Environment whose changes were invalidated,
|
|
e.g. ``"default"``.
|
|
"""
|
|
|
|
type: Literal["session.changed_files.invalidated"]
|
|
session_id: str
|
|
# "default" is the canonical primary-environment id
|
|
# (DEFAULT_ENVIRONMENT_ID); the changed-files panel only tracks that
|
|
# environment, so it's the sole expected value, not an invented one.
|
|
environment_id: str = "default"
|
|
|
|
|
|
class SessionTerminalActivityEvent(_SSEEventBase):
|
|
"""
|
|
A terminal's pane produced output (runner-determined activity pulse).
|
|
|
|
Powers the web "active" badge for any terminal without a client PTY
|
|
attach — the runner's per-terminal pane watcher emits this when the
|
|
pane content changes. Transient (a live pulse; not persisted, not in
|
|
the connect snapshot).
|
|
|
|
:param type: Always ``"session.terminal.activity"``.
|
|
:param session_id: Owning session/conversation id.
|
|
:param terminal_id: Opaque terminal resource id, e.g.
|
|
``"terminal_zsh_s1"``.
|
|
"""
|
|
|
|
type: Literal["session.terminal.activity"]
|
|
session_id: str
|
|
terminal_id: str
|
|
|
|
|
|
class TurnStartedEvent(_SSEEventBase):
|
|
"""
|
|
Emitted when the runner starts a new turn for a session.
|
|
|
|
:param type: Fixed literal ``"turn.started"``.
|
|
:param session_id: Session/conversation identifier,
|
|
e.g. ``"conv_abc123"``.
|
|
"""
|
|
|
|
type: Literal["turn.started"]
|
|
session_id: str
|
|
|
|
|
|
class TurnCompletedEvent(_SSEEventBase):
|
|
"""
|
|
Emitted when a turn finishes successfully with no pending work.
|
|
|
|
:param type: Fixed literal ``"turn.completed"``.
|
|
:param session_id: Session/conversation identifier,
|
|
e.g. ``"conv_abc123"``.
|
|
"""
|
|
|
|
type: Literal["turn.completed"]
|
|
session_id: str
|
|
|
|
|
|
class TurnFailedEvent(_SSEEventBase):
|
|
"""
|
|
Emitted when a turn fails due to an LLM error, timeout, or crash.
|
|
|
|
:param type: Fixed literal ``"turn.failed"``.
|
|
:param session_id: Session/conversation identifier,
|
|
e.g. ``"conv_abc123"``.
|
|
:param error: Error details, e.g.
|
|
``{"message": "LLM timeout", "type": "TimeoutError"}``.
|
|
"""
|
|
|
|
type: Literal["turn.failed"]
|
|
session_id: str
|
|
error: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class TurnCancelledEvent(_SSEEventBase):
|
|
"""
|
|
Emitted when a turn is interrupted by the user or system.
|
|
|
|
:param type: Fixed literal ``"turn.cancelled"``.
|
|
:param session_id: Session/conversation identifier,
|
|
e.g. ``"conv_abc123"``.
|
|
"""
|
|
|
|
type: Literal["turn.cancelled"]
|
|
session_id: str
|
|
|
|
|
|
# ── Discriminated union ─────────────────────────────────────────────
|
|
|
|
|
|
# ServerStreamEvent: every event a stream consumer (AP-as-harness-
|
|
# client OR an external client of AP) may receive on either of
|
|
# AP's two SSE endpoints. Pydantic dispatches on the ``type``
|
|
# field via ``Field(discriminator="type")``; each variant's
|
|
# ``Literal[...]`` pins the correct branch.
|
|
#
|
|
# Usage:
|
|
# from pydantic import TypeAdapter
|
|
# from omnigent.server.schemas import ServerStreamEvent
|
|
# adapter = TypeAdapter(ServerStreamEvent)
|
|
# event = adapter.validate_python(raw_dict)
|
|
# # ``event`` is now the concrete typed model.
|
|
#
|
|
# Renamed from ``ResponseStreamEvent`` to disambiguate from the
|
|
# OpenAI SDK's identically-named type used inside
|
|
# ``omnigent.llms.types``.
|
|
ServerStreamEvent = Annotated[
|
|
# ── Transient (SSE-only) — session.* lifecycle ─────────────
|
|
SessionStatusEvent
|
|
| SessionUsageEvent
|
|
| SessionModelEvent
|
|
| SessionReasoningEffortEvent
|
|
| SessionCollaborationModeEvent
|
|
| SessionAgentChangedEvent
|
|
| SessionTodosEvent
|
|
| SessionTerminalPendingEvent
|
|
| SessionSandboxStatusEvent
|
|
| SessionMcpStartupEvent
|
|
| SessionSkillsEvent
|
|
| SessionModelOptionsEvent
|
|
| SessionInputConsumedEvent
|
|
| SessionInterruptedEvent
|
|
| SessionCreatedEvent
|
|
| SessionSupersededEvent
|
|
| SessionPresenceEvent
|
|
# ── Transient (SSE-only) — session resource lifecycle ─────
|
|
| SessionResourceCreatedEvent
|
|
| SessionResourceDeletedEvent
|
|
| SessionChildSessionUpdatedEvent
|
|
| SessionChangedFilesInvalidatedEvent
|
|
| SessionTerminalActivityEvent
|
|
# ── Transient (SSE-only) — incremental token deltas ────────
|
|
| OutputTextDeltaEvent
|
|
| ReasoningStartedEvent
|
|
| ReasoningTextDeltaEvent
|
|
| ReasoningSummaryTextDeltaEvent
|
|
# ── Persistent (POST + SSE replay) — wraps conv-store items
|
|
| OutputItemDoneEvent
|
|
# ── Transient (SSE-only) — file annotations / keepalive ────
|
|
| OutputFileDoneEvent
|
|
| HeartbeatEvent
|
|
| SessionHeartbeatEvent
|
|
# ── Transient (SSE-only) — synchronous decision request ────
|
|
| ElicitationRequestEvent
|
|
| ElicitationResolvedEvent
|
|
# ── Transient (SSE-only) — embedded-browser action request ─
|
|
| BrowserActionRequestEvent
|
|
# ── Transient (SSE-only) — native policy DENY signal ───────
|
|
| PolicyDeniedEvent
|
|
# ── Transient (SSE-only) — Responses-API turn lifecycle ────
|
|
| CreatedEvent
|
|
| QueuedEvent
|
|
| InProgressEvent
|
|
| CompletedEvent
|
|
| FailedEvent
|
|
| CancelledEvent
|
|
| IncompleteEvent
|
|
# ── Transient (SSE-only) — operational signals ─────────────
|
|
| RetryEvent
|
|
| ErrorEvent
|
|
| CompactionInProgressEvent
|
|
| CompactionCompletedEvent
|
|
| CompactionFailedEvent
|
|
| ClientTaskCancelEvent
|
|
| TurnStartedEvent
|
|
| TurnCompletedEvent
|
|
| TurnFailedEvent
|
|
| TurnCancelledEvent,
|
|
Field(discriminator="type"),
|
|
]
|
|
|
|
|
|
# Frozen set of every wire ``type`` literal across the union.
|
|
# Derived from :data:`ServerStreamEvent` so adding a new event variant
|
|
# to the union automatically updates the drift-detection set — there
|
|
# is no second list to keep in sync.
|
|
#
|
|
# ``ServerStreamEvent`` is ``Annotated[A | B | ..., Field(...)]``;
|
|
# ``get_args`` returns ``(A | B | ..., FieldInfo)``. The first element
|
|
# is the union, whose own ``get_args`` yields the variant classes.
|
|
_KNOWN_EVENT_TYPES: frozenset[str] = frozenset(
|
|
# ``model_fields["type"].annotation`` is the ``Literal[...]``
|
|
# carried by each variant; ``.__args__[0]`` extracts the
|
|
# single string literal.
|
|
cls.model_fields["type"].annotation.__args__[0]
|
|
for cls in get_args(get_args(ServerStreamEvent)[0])
|
|
)
|
|
|
|
|
|
def is_known_event(name: str) -> bool:
|
|
"""
|
|
Return whether ``name`` is a wire ``type`` literal in the union.
|
|
|
|
Used by the drift-detection test
|
|
(``tests/server/test_stream_events.py``): integration tests
|
|
patch :func:`omnigent.runtime.session_stream.publish` to
|
|
call this on every emitted ``event["type"]``; any string not
|
|
in the union fails the test, catching new emissions that
|
|
bypassed the source of truth.
|
|
|
|
:param name: Candidate wire name to check, e.g.
|
|
``"response.output_text.delta"``.
|
|
:returns: ``True`` if ``name`` is the ``type`` literal of a
|
|
:data:`ServerStreamEvent` variant, ``False`` otherwise.
|
|
"""
|
|
return name in _KNOWN_EVENT_TYPES
|
|
|
|
|
|
# Events the harness may emit on its per-turn SSE stream that are
|
|
# runner-internal: the runner intercepts and consumes them (matching by
|
|
# ``type`` on the raw frame, see ``omnigent/runner/app.py`` proxy_stream)
|
|
# and never relays them to clients. They are deliberately NOT part of the
|
|
# public :data:`ServerStreamEvent` union / openapi. This alias types the
|
|
# scaffold's per-turn event queue, which carries both the public events and
|
|
# these internal markers. See ``designs/RUNNER_MESSAGE_INGEST.md`` Part B.
|
|
|
|
|
|
class PolicyEvaluationRequestEvent(_SSEEventBase):
|
|
"""
|
|
Runner-internal marker: harness requests policy evaluation.
|
|
|
|
Emitted by the executor adapter before or after an LLM call so
|
|
the runner can evaluate ``LLM_REQUEST`` / ``LLM_RESPONSE``
|
|
policies on the Omnigent server. The runner intercepts this event in
|
|
``proxy_stream``, calls the Omnigent server's
|
|
``POST /sessions/{id}/policies/evaluate`` endpoint, and posts
|
|
the verdict back to the harness as a ``policy_verdict`` inbound
|
|
event. This event is **never** relayed to external clients —
|
|
it is purely a runner↔harness handshake.
|
|
|
|
:param type: Always ``"policy_evaluation.requested"``.
|
|
:param evaluation_id: Unique correlation id for this
|
|
evaluation, e.g. ``"poleval_abc123"``. The runner echoes
|
|
it back in the ``policy_verdict`` inbound event so the
|
|
scaffold can resolve the correct parked Future.
|
|
:param phase: Proto-style phase string, e.g.
|
|
``"PHASE_LLM_REQUEST"`` or ``"PHASE_LLM_RESPONSE"``.
|
|
:param data: Event data dict passed to the Omnigent server's
|
|
policy evaluate endpoint, e.g.
|
|
``{"model": "gpt-4o", "messages_count": 42}``.
|
|
"""
|
|
|
|
type: Literal["policy_evaluation.requested"]
|
|
evaluation_id: str
|
|
phase: str
|
|
data: dict[str, Any]
|
|
|
|
|
|
HarnessStreamEvent = ServerStreamEvent | InjectionConsumedEvent | PolicyEvaluationRequestEvent
|