Operator Architecture
Framework-agnostic multi-agent orchestration SDK.
Operator Architecture (OA) manages state, context, sub-agents, and orchestration. It is compatible with any agent runtime — Relay, LangChain, OpenAI Agents, HTTP services, or a plain async function.
Install
uv add operator-architecture
# or
pip install operator-architecture
Runtime dependencies: none (stdlib only).
from operator_architecture import (
StateMachine,
Coordinator,
AgentSpec,
AgentRequest,
AgentResult,
callable_agent,
)
What OA owns vs what you own
| Operator Architecture | Your host |
|---|---|
StateMachine, Coordinator, AgentSpec |
Agent implementations (AgentRunner) |
| OpenAI-compatible message threads | Relay / LangChain / custom loops |
commission → stage → accept / instruct |
Models, API keys, tools, MCP, FS |
Optional streaming_callback fan-in |
Emitting stream events from runners |
Core objects
AgentSpec + AgentRunner
Register any number of sub-agents. Each needs a runner OA will call:
async def research(request: AgentRequest) -> AgentResult:
# call Relay, LangChain, HTTP, … — OA does not care
return AgentResult(content=f"Findings for: {request.objective}")
researcher = AgentSpec(
name="researcher",
description="Read-only exploration",
skill="You are a careful researcher. Answer with concrete findings.",
runner=callable_agent(research),
model="my-model", # metadata only
)
Protocol:
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.
Coordinator
Owns the user-facing skill string and optional runner for sm.run():
coordinator = Coordinator(
skill="You operate the state machine…", # default skill provided
runner=my_coordinator_runner, # optional
model="coord-model",
)
StateMachine
sm = StateMachine(coordinator=coordinator, agents=[researcher])
No process-global singleton — hold the instance yourself (sm = StateMachine(...)).
Lifecycle
user → (optional) sm.run / host
→ commission(agent, objective)
→ AgentRunner.run(AgentRequest)
→ stage agent_message (status=staged)
→ tool result includes summary (full) + preview (first 400 chars)
→ accept_agent_result(agent, index) # same full summary, status=accepted
or instruct_agent(agent, index, message) # continue junior
The junior’s full session stays on the slot (sm.agent(name)[index].messages). Core context only sees what the host stores as the orchestration role: "tool" body. That body now carries summary (the full junior final message), so a normal tool loop does not truncate follow-up memory.
Tool args
OpenAI schemas keep their intended types (index integer, checklist array, agent_props object). At runtime OA also accepts stringified LLM values ("1", "{\"k\": \"v\"}"). Invalid values return { "error": ... } instead of raising TypeError.
Tool result
staged = await sm.commission("researcher", "Find all uses of vLLM")
# staged["summary"] == full agent_message (no cap)
# staged["preview"] == first 400 chars (short alias)
# staged["status"] == "staged"
accepted = sm.accept("researcher", 1) # index=1 and index="1" both work
# accepted["summary"] == same full text
# accepted["result"]["report"] == same full text
Direct API (always available)
staged = await sm.commission("researcher", "Find all uses of vLLM")
msg = sm.get_agent_message("researcher", 1)
accepted = sm.accept("researcher", 1) # or accept_agent_result
# or:
await sm.instruct("researcher", 1, "Also check Dockerfiles")
sm.list_agents()
sm.list_objectives()
Indexed access: sm.agent("researcher")[1].agent_message.
sm.run (optional)
If Coordinator.runner is set, await sm.run(user_text, streaming_callback=...) appends the user message and invokes that runner with:
metadata["tools"]— orchestration callablesmetadata["tool_schemas"]— OpenAItools[]schemas
OA does not execute a tool loop. Your runner (Relay, LangChain, …) must invoke those callables when the model requests them.
tools = sm.orchestration_tools()
# list_agents, commission, get_agent_message,
# accept_agent_result, instruct_agent, list_objectives
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()).
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.commission("researcher", "…", streaming_callback=on_stream)
# or
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.
Writing adapters
| Adapter idea | Wraps |
|---|---|
callable_agent(fn) |
Plain async/sync function (shipped) |
| Relay agent | encode.relay_async / courier_os.relay inside run() |
| LangChain agent | AgentExecutor / LangGraph; map messages ↔ LC messages |
| HTTP agent | POST OpenAI-compatible or custom JSON API |
OA never imports those libraries. Keep adapters in your host.
Minimal Relay sketch (host code)
class RelayRunner:
def __init__(self, model, api_key, base_url, tools):
self.model, self.api_key, self.base_url, self.tools = model, api_key, base_url, tools
async def run(self, request, *, streaming_callback=None):
import encode
messages = encode.Messages()
for m in request.messages:
# map dicts into encode.Messages as needed
...
out = await encode.relay_async(
model=self.model,
api_key=self.api_key,
base_url=self.base_url,
messages=messages,
tools=self.tools or request.metadata.get("tools"),
)
return AgentResult(content=out.content or "", raw=out)
Example
See examples/minimal_callable.py.
License / status
Early SDK (0.2.0). API may evolve; the orchestration contract (commission / stage / accept) is the stable idea.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file operator_architecture-0.3.0.tar.gz.
File metadata
- Download URL: operator_architecture-0.3.0.tar.gz
- Upload date:
- Size: 14.3 kB
- Tags: Source
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6022bacebeaf420a2234cfa9a6e59b66bb635407f94a2c9621fc61f4fefc01e1
|
|
| MD5 |
0ccf235bf62b0577ce939d5cae65af84
|
|
| BLAKE2b-256 |
c0c91082d6ad4e79cfb09eea27606ba03163ea50c385eb0e1815fffe448bb252
|
File details
Details for the file operator_architecture-0.3.0-py3-none-any.whl.
File metadata
- Download URL: operator_architecture-0.3.0-py3-none-any.whl
- Upload date:
- Size: 14.4 kB
- Tags: Python 3
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
78520b7c3dd836dc537266bc022efb3bb317f68afe954395da3924b5f2656248
|
|
| MD5 |
a6eb77a953080e6268d0f68f71f271c1
|
|
| BLAKE2b-256 |
dbc212b010591baacb703e96f9d4388f55fde3fa6d44c13d53c9e0c337301c62
|