Operator Architecture
Framework-agnostic multi-agent orchestration SDK.
Operator Architecture (OA) manages state, context, sub-agents, and orchestration. The product API is one call: await sm.run(user_text). That turn is the full agentic loop — the coordinator receives the user message, commissions sub-agents, instructs or accepts their work, and replies.
Compatible with any agent runtime — Relay, LangChain, OpenAI Agents, HTTP services, or a plain async function. Runtime dependency: Pydantic v2.
Install
uv add operator-architecture
# or
pip install operator-architecture
from operator_architecture import (
StateMachine,
Coordinator,
AgentSpec,
AgentRequest,
AgentResult,
callable_agent,
)
The loop: sm.run
Every agent is an AgentRunner: AgentRequest in, AgentResult out. Pass one to the coordinator and one to each junior, then call sm.run. Hosts should not drive commission / accept / instruct themselves — that is what the coordinator does inside the turn.
class CoordinatorRunner:
async def run(self, request: AgentRequest, *, streaming_callback=None) -> AgentResult:
# Handle the agentic loop here.
# request.messages — coordinator thread so far
# request.metadata["tools"] / ["tool_schemas"] — commission, instruct, accept, ...
# Call your model, invoke those tools, repeat until you can reply to the user.
return AgentResult(content="...")
class ResearchRunner:
async def run(self, request: AgentRequest, *, streaming_callback=None) -> AgentResult:
# Junior work: Relay, LangChain, HTTP, ...
return AgentResult(content=f"Findings for: {request.objective}")
sm = StateMachine(
coordinator=Coordinator(runner=CoordinatorRunner()),
agents=[
AgentSpec(
name="researcher",
description="Read-only exploration",
skill="You are a careful researcher. Answer with concrete findings.",
runner=ResearchRunner(),
),
],
)
reply = await sm.run("Map where vLLM is configured")
OA does not ship an LLM. sm.run is the turn API; Coordinator.runner is where the model + tool iteration lives. If that runner ignores request.metadata["tools"], juniors never run.
Full runnable copy: examples/minimal_callable.py.
What sm.run owns
await sm.run(
user_text,
*,
streaming_callback=None,
orchestration_tools=True,
) -> str
One call is one operator turn:
- Requires
Coordinator.runner. Unset →RuntimeError. Empty / whitespace input →"". - Appends the user message to
sm.coordinator_messages. - Builds an
AgentRequest(agent="coordinator",objective=user_text, coordinator skill, current messages). - Injects orchestration tools when
orchestration_tools=True(the default):request.metadata["tools"]— callablesrequest.metadata["tool_schemas"]— OpenAItools[]schemas
- Invokes
Coordinator.runner.run(...)once. That runner must loop on tool calls until it has a user-facing reply. - Persists the result onto the coordinator thread:
result.messagesreplaces the thread; otherwiseresult.contentis appended as an assistant message. - Returns the reply string (or
"(no response)").
host: await sm.run(user_text)
→ append user message
→ Coordinator.runner
→ model round
→ commission / instruct_agent / accept_agent_result
→ junior AgentRunners
→ …until a final assistant message
→ persist reply on coordinator thread
→ return str
Coordinator runner
You supply inference. OA supplies the orchestration tools the coordinator must call:
| Tool | Role |
|---|---|
list_agents() |
Discover registered juniors |
commission(agent, objective, checklist=None, agent_props=None) |
Run a junior; result is staged |
get_agent_message(agent, index) |
Peek staged prose without accepting |
instruct_agent(agent, index, message) |
Continue the same objective |
accept_agent_result(agent, index) |
Accept into the core thread |
list_objectives(agent=None) |
Status board |
commission / instruct return summary (full junior final message, no cap). OpenAI schemas keep native types (index integer, checklist array, agent_props object); at runtime OA also accepts stringified LLM values ("1", "{\"k\": \"v\"}"). Invalid args return { "error": ... } instead of raising.
Default coordinator skill already describes this loop. Override Coordinator.skill only if you need a different persona.
Return AgentResult.messages when you own the full coordinator transcript (typical for a real tool loop). Return only content when you do not.
Juniors: AgentSpec + AgentRunner
Each sub-agent is a named spec whose runner OA calls on commission / instruct:
class AgentRunner(Protocol):
async def run(
self,
request: AgentRequest,
*,
streaming_callback: StreamingCallback = None,
) -> AgentResult: ...
AgentRequest carries OpenAI-shaped messages, objective, skill, optional checklist / agent_props.
AgentResult.content is staged as agent_message. The junior’s full session stays on the slot (sm.agent(name)[index].messages). Core context only sees what the coordinator’s tool loop stores (the orchestration tool result, including summary).
callable_agent(fn) wraps a plain async/sync function. For Relay, LangChain, or HTTP, keep the adapter in your host — OA never imports those libraries.
Manual APIs available if required
sm.commission, sm.accept / sm.accept_agent_result, sm.instruct, sm.list_agents, and sm.list_objectives exist for tests and debugging. Do not use them as a second way to run OA. The coordinator inside sm.run is supposed to call those tools.
streaming_callback
Optional observability hook (UI, logs, websockets). Sync or async:
async def on_stream(event: dict) -> None:
print(event["phase"], event.get("detail", "")[:80])
await sm.run("…", streaming_callback=on_stream)
Phases: start, token, tool_start, tool_result, tool_error, commissioned, done, fail.
Runners may emit events; OA forwards them and also emits lifecycle events around commission.
OpenAI-compatible context
Coordinator and junior threads are lists of chat.completions-style dicts:
{"role": "system"|"user"|"assistant"|"tool", "content": "...", ...}
Helpers: Messages (.system(), .user(), .assistant(), .to_list()).
Serialize / rehydrate
Data types (AgentRequest, AgentResult, AgentSpec, Coordinator, ObjectiveSlot, AgentHandle) are Pydantic v2 models. StateMachine is still the engine — snapshot it with helpers, then rebound host runners on load. Runners, streaming callbacks, and orchestration-tool closures are never pickled.
from operator_architecture import (
StateMachine,
state_machine_to_json,
state_machine_from_json,
)
runners = {"coordinator": CoordinatorRunner(), "researcher": ResearchRunner()}
blob = state_machine_to_json(sm)
sm2 = state_machine_from_json(blob, runners=runners)
sm.to_model() / StateMachine.from_model(model, *, runners=...) are the same path without JSON. Snapshots include schema_version (currently 1), coordinator persona and thread, active_agent, and each junior's spec + slots.
Rebind keys with runner_id (defaults to the agent name; coordinator defaults to "coordinator" when a runner is set). Missing ids raise KeyError. Pass coordinator_runner= to override the mapping. Restore streaming_callback on load if you still want events; tools are rebuilt on the next sm.run().
Nested helpers: agent_spec_to_model / agent_spec_from_model, coordinator_*, agent_handle_*, messages_*. Snapshot models: StateMachineModel, AgentSpecModel, CoordinatorModel, AgentHandleModel, ChatMessage (extra="allow" so tool_calls survive).
Appendix: SDK spec
Public surface from operator_architecture (__all__). Field lists are constructor / Pydantic fields unless noted.
Coordinator
Pydantic model. Persona handed to StateMachine.
skill: str = DEFAULT_COORDINATOR_SKILL— coordinator system promptrunner: AgentRunner | None = None— required forsm.run; excluded from dumpsrunner_id: str | None = None— snapshot key; defaults to"coordinator"whenrunneris setmodel: str | None = None— metadata onlymetadata: dict[str, Any] = {}— merged into the coordinatorAgentRequest
AgentSpec
Pydantic model. Registers one junior.
name: str— unique; must not be"coordinator"description: str— shown bylist_agentsskill: str— junior system promptrunner: AgentRunner— called on commission / instruct; excluded from dumpsrunner_id: str | None = None— snapshot key; defaults tonamemodel: str | None = None— metadata onlymetadata: dict[str, Any] = {}— copied onto juniorAgentRequest
AgentRunner
Protocol. Anything with this method counts (class, callable_agent(fn), …).
async def run(
self,
request: AgentRequest,
*,
streaming_callback: StreamingCallback = None,
) -> AgentResult: ...
AgentRequest
Pydantic model. Input to every runner.
agent: strobjective: strskill: strmessages: list[dict[str, Any]]— OpenAI-shaped threadchecklist: list[str] | None = Noneagent_props: dict[str, Any] | None = Nonemodel: str | None = Nonemetadata: dict[str, Any] = {}— coordinator requests also gettoolsandtool_schemas
AgentResult
Pydantic model. Output from every runner.
content: str— user-facing reply (coordinator) or stagedagent_message(junior)messages: list[dict[str, Any]] | None = None— if set, replaces the threadusage: dict[str, Any] | None = Noneraw: Any = None— host-only; excluded frommodel_dump()/ snapshots
StateMachine
StateMachine(
coordinator: Coordinator | None = None,
agents: Sequence[AgentSpec] | None = None,
)
Defaults to Coordinator() and no juniors. No process-global singleton.
Instance attrs
coordinator: Coordinatorcoordinator_messages: Messagesactive_agent: str | None
Turn / registry
async run(user_text, *, streaming_callback=None, orchestration_tools=True) -> stradd_agent(spec: AgentSpec) -> AgentHandleagent(name: str) -> AgentHandle— 1-based slots:sm.agent(name)[index]agents() -> list[AgentHandle]set_streaming_callback(callback: StreamingCallback) -> Noneorchestration_tools() -> list— callables + OpenAI schemas for the coordinator runnerto_model() -> StateMachineModel— snapshot (no live runners or callbacks)StateMachine.from_model(model, *, runners, coordinator_runner=None, streaming_callback=None) -> StateMachine
Last-resort (tests / debugging)
async commission(agent, objective, checklist=None, agent_props=None, *, streaming_callback=None) -> dictasync instruct(agent, index, message, *, streaming_callback=None) -> dictasync instruct_agent(...)— alias ofinstructaccept(agent, index) -> dict/accept_agent_result(agent, index) -> dictget_agent_message(agent, index) -> dictlist_agents() -> list[dict]list_objectives(agent=None) -> list[dict]
AgentHandle
Pydantic model. Runtime handle for one registered junior.
spec: AgentSpecslots: list[ObjectiveSlot] = []name—spec.namerunner—spec.runnerhandle[index] -> ObjectiveSlot— 1-based (indexint or"1")objectives() -> list[ObjectiveSlot]len(handle)— slot count
ObjectiveSlot
Pydantic model. One commissioned objective.
index: int— 1-basedagent: strobjective: strchecklist: list[str] = []agent_props: dict[str, Any] = {}messages: list[dict[str, Any]] = []— junior threadagent_message: str | None = None— last junior proseresult: dict[str, Any] | None = Nonestatus: SlotStatus = "pending"—pending | running | staged | accepted | failedmodel: str | None = Noneduration_ms: float | None = Nonecommission_id: str = ""— defaults to"{agent}-{index}"
Messages / Message
Message is dict[str, Any] (chat.completions shape).
Messages(messages: list[Message] | None = None)
system(content) -> Messagesuser(content) -> Messagesassistant(content, *, tool_calls=None) -> Messagestool(content, *, tool_call_id) -> Messagesappend(message) -> Messages/extend(messages) -> Messagesreplace(messages) -> Messagesto_list() -> list[Message]— deep copyclear() -> None
StreamEvent / StreamingCallback
StreamEvent is a TypedDict (all keys optional):
agent: strphase: str—start | token | tool_start | tool_result | tool_error | commissioned | done | faildetail: strmodel: strobjective: strcommission_id: strstatus: strduration_ms: floatindex: int
StreamingCallback — Callable[[StreamEvent], None] or async equivalent, or None.
Helpers
callable_agent(fn) -> AgentRunner— wrapfn(request) -> AgentResult | str(sync or async)emit_stream(callback, event) -> None— no-op ifcallbackisNoneopenai_tool_schema(fn, *, name=None, description=None) -> dict— chat.completionstools[]entrytool_schemas(tools) -> list[dict]— schemas for a list of callablesDEFAULT_COORDINATOR_SKILL: str— defaultCoordinator.skillstate_machine_to_model(sm) -> StateMachineModel/state_machine_from_model(model, *, runners, ...) -> StateMachinestate_machine_to_json(sm) -> str/state_machine_from_json(data, *, runners, ...) -> StateMachineagent_spec_to_model/agent_spec_from_model,coordinator_to_model/coordinator_from_model,agent_handle_to_model/agent_handle_from_model,messages_to_model/messages_from_model
Snapshot models
StateMachineModel—schema_version: Literal[1] = 1,coordinator,coordinator_messages,active_agent,agentsAgentSpecModel— spec withoutrunner, withrunner_idCoordinatorModel— coordinator withoutrunner, with optionalrunner_idAgentHandleModel—spec: AgentSpecModel+slots: list[ObjectiveSlot]ChatMessage— OpenAI-shaped message; extra fields allowed
License / status
Early SDK (0.4.0). API may evolve; sm.run as the operator turn is the stable idea.
Metadata
Release files for operator-architecture 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| operator_architecture-0.4.0.tar.gz | 31.2 kB | Without provenance | Details for operator_architecture-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| operator_architecture-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for operator_architecture-0.4.0-py3-none-any.whl |
Total release size: 50.8 kB
Release files / operator_architecture-0.4.0.tar.gz
| Download URL | operator_architecture-0.4.0.tar.gz |
|---|---|
| Size | 31.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cd7c54e7e9adbc04b0565787da0b530eca15bf9c3a64c69affa6dca7f00e4101
|
|
BLAKE2b-256 checksum How to use checksums |
c9b31d9f24f613f7cf8c42632a58d47e525e2b7afdff5dd6321f05c7eb34abcc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / operator_architecture-0.4.0-py3-none-any.whl
| Download URL | operator_architecture-0.4.0-py3-none-any.whl |
|---|---|
| Size | 19.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fbcbc80e4d3cea570af57a65737aed8931febe00823996f6f96e5a46380b5ab6
|
|
BLAKE2b-256 checksum How to use checksums |
1e24ed675a8811891e063c760ed4f6318787983f07da22964db8730513852f16
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for operator-architecture 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.