Skip to main content

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 callables
  • metadata["tool_schemas"] — OpenAI tools[] 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

operator_architecture-0.3.0.tar.gz (14.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

operator_architecture-0.3.0-py3-none-any.whl (14.4 kB view details)

Uploaded Python 3

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

Hashes for operator_architecture-0.3.0.tar.gz
Algorithm Hash digest
SHA256 6022bacebeaf420a2234cfa9a6e59b66bb635407f94a2c9621fc61f4fefc01e1
MD5 0ccf235bf62b0577ce939d5cae65af84
BLAKE2b-256 c0c91082d6ad4e79cfb09eea27606ba03163ea50c385eb0e1815fffe448bb252

See more details on using hashes here.

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

Hashes for operator_architecture-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 78520b7c3dd836dc537266bc022efb3bb317f68afe954395da3924b5f2656248
MD5 a6eb77a953080e6268d0f68f71f271c1
BLAKE2b-256 dbc212b010591baacb703e96f9d4388f55fde3fa6d44c13d53c9e0c337301c62

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page