# OpenRath

OpenRath logo

PyPI Python License Docs arXiv

English | [简体中文](README_zh.md)
--- **OpenRath is a PyTorch-like multi-agent & multi-session framework.** It turns agent runtime state into explicit, composable Python objects: - **Session** carries conversation state and inter-agent collaboration lineage. - **Sandbox** decides where tools actually run. - **Memory** persists agent memory state across runs. - **Tool** is the operator-like callable surface exposed to the model. - **Agent** is a reusable, composable session transformation layer. - **Workflow** composes multiple agents and workflows into larger systems. - **Selector** routes between self-describing workflows at runtime, so `if` / `while` control flow stays plain Python. --- ## OpenRath in the PyTorch Lens

OpenRath in the PyTorch Lens

| PyTorch idea | OpenRath idea | What it means | | --- | --- | --- | | `Tensor` | `Session` | The flowing runtime value: ordered chunks, placement, lineage, and usage. | | `Device` | `Sandbox` / `Backend` | The execution environment where tools run: local process, OpenSandbox, or another backend. | | `Parameter` | `Memory` | Persistent state bound to an agent or store, recalled and committed across runs. | | `Function` | `Tool` | A callable operation with model-visible schema and runtime behavior. | | `nn.Linear` | `Agent` | A reusable layer that maps one session to another using a prompt, provider, tools, and memory. | | `nn.Module` | `Workflow` | A composable container for agents, tools, session transforms, and nested workflows. | | control flow | `Selector` | An LLM-backed router that picks the next workflow at runtime, enabling dynamic `if` / `while` over agents. | Most agent frameworks begin with an agent loop. OpenRath begins with **Session**. That difference matters when one application needs multiple agents, multiple branches, durable memory, sandboxed execution, and traceable lineage at the same time. OpenRath is designed for this: many agents collaborating across many branchable sessions, while still tracing every role, workspace, memory write, and final output. ## OpenRath v2.0.0: Built for Production The defining change in OpenRath v2.0.0 is that OpenRath moves beyond a composable Python framework and becomes a durable runtime designed for production deployment. The existing Session-first Python API remains intact; the release adds a production execution and operations layer around it.

OpenRath v2.0.0 durable runtime overview

The diagram summarizes this production path: Python definitions compile into immutable plans, durable Runs are governed by checkpoints, leases, effects, and interrupts, and PostgreSQL, Redis, and S3-compatible storage provide the operational data plane. | Production concern | What OpenRath provides | | --- | --- | | Durable execution | Explicit `@step` / `@router` boundaries compile into immutable execution plans. Runs, Events, and Checkpoints survive process and worker restarts. | | Resilient workers | Leases, fencing, retries, cancellation, deadlines, and resumable queues prevent stale workers from silently committing new state. | | Controlled side effects | An Effect Ledger records outcomes and idempotency keys. Ambiguous non-idempotent effects stop in `NEEDS_REVIEW` instead of being replayed blindly. | | Human decisions | Durable Interrupts pause a Run for approval or input and resume it without rebuilding hidden loop state. | | Security and tenancy | Agent Server tokens carry explicit action grants; tenant/project scope, policy checks, secret references, trust labels, and audit remain separate boundaries. | | Production operations | PostgreSQL is the durable source of truth, Redis can accelerate signaling, S3-compatible storage holds artifacts, and health, migration, telemetry, container, and Kubernetes references are included. | Embedded mode remains useful inside a trusted process. Agent Server mode is the strict production profile: ```python runtime = LocalRuntime( store, effect_ledger=ledger, production_mode=True, ) server = AgentServer(store, runtime, auth=auth, audit_sink=audit) ``` Install the production profile and run schema migration as a separate operation: ```bash pip install "openrath[server,postgres]" openrath-migrate openrath-migrate --check ``` Runtime identities do not need DDL privileges. Tokens need explicit action grants, object access is tenant/project scoped, and synchronous steps cannot declare a preemptive timeout; use an async step or isolated executor when a deadline must be enforced. OpenRath v2.0.0 is designed for production deployment while keeping interface maturity explicit: the Agent Server HTTP surface remains **Beta**, and v1 JSONL imports are historical records rather than resumable active Runs. Deployment, migration, security, and operations guidance lives in [`deploy/`](deploy/), including [`operations-v2.md`](deploy/docs/operations-v2.md), [`migration-v2.md`](deploy/docs/migration-v2.md), and the generated [`openapi-v2.json`](deploy/docs/openapi-v2.json).

Multi-Agent Multi-Session Map

| Paradigm | Typical shape | Example | | --- | --- | --- | | Single agent, single session | One model over one conversation | ChatGPT-style chat | | Multi-agent, single session | Several roles read and write one shared state | Sub-agent-style multi-agent collaboration | | Single agent, multi-session | One agent manages many session branches | OpenClaw-style session fanout | | Multi-agent, multi-session | Many agents share many sessions and collaborate or evolve through Session | **OpenRath** | --- ## Why the Multi-Agent-Multi-Session Paradigm An agent is a transformation layer on Session, so what really needs to be forked, merged, reused, and traced is the Session dataflow—not a separate message history maintained by each agent.

Why Multi-Agent Multi-Session

OpenRath's design addresses the problems that appear when agent systems move from one assistant to large clusters: - **Session as the dataflow core.** Context is stored as structured chunks rather than repeatedly copied message strings. Workflows can reuse, fork, compress, and pass context directly, which greatly improves context reuse and reduces token consumption. - **Session Graph for massive agent clusters.** Large runs need to explain which role, branch, tool call, and workspace produced an answer. Session lineage gives the runtime a graph-shaped provenance layer instead of a pile of post-hoc logs. - **Session plus Agent Memory.** Short-term session state and long-term memory work together: agents can recall facts before a run and commit new knowledge afterward, so an agent cluster can keep improving as it is used. - **Modular Workflow.** Managing hundreds or thousands of agents becomes a composition problem instead of prompt spaghetti. Agents are small layers; workflows are nested, reusable, inspectable modules that make large-scale agent management tractable. - **Sandbox as a backend.** Execution is not hardcoded to one shell. Local, OpenSandbox, or future third-party backends can sit behind the same session placement model, so third-party execution backends plug in flexibly. - **Memory as a backend.** Recall is not hardcoded to one database. Local memory, OpenViking, or future third-party memory systems can share one memory plane, so third-party memory backends plug in flexibly. The result is a runtime where state, execution, memory, and orchestration stay decoupled enough to scale, yet remain connected through one flowing value: `Session`. --- ## A Minimal but Complete OpenRath Workflow ```python from collections.abc import Mapping from typing import Any from pydantic import BaseModel, Field from rath import flow from rath.flow.tool import FlowToolCall from rath.session import Session class WordCountInput(BaseModel): text: str = Field(description="Text to count.") # OpenRath Tool class WordCountTool(FlowToolCall): @property def name(self) -> str: return "word_count" @property def description(self) -> str: return "Count words in a short text." @property def parameters(self) -> Mapping[str, Any]: return WordCountInput.model_json_schema() def __call__(self, session: Session, arguments: Mapping[str, Any]) -> dict[str, int]: data = WordCountInput.model_validate(dict(arguments)) return {"words": len(data.text.split())} # OpenRath Workflow class ReadmeWorkflow(flow.Workflow): def __init__(self) -> None: # OpenRath Provider provider = flow.Provider(model="gpt-5.5") # OpenRath Agent self.agent = flow.Agent( "Use the `word_count` tool, then answer briefly.", provider, tools=[WordCountTool()], memory="local", ) # OpenRath Compressor self.compressor = flow.Compressor( "Compress the run into one concise assistant message.", provider, ) def forward(self, session: Session) -> Session: # OpenRath Memory self.agent.remember_memory("The user likes compact technical summaries.") session = self.agent(session) self.agent.commit_memory(session) return self.compressor(session) workflow = ReadmeWorkflow() # OpenRath Session user_session = Session.from_user_message( "Count the words in: OpenRath makes agent clusters traceable." ) # OpenRath Sandbox user_session = user_session.to("local", spec="./") out = workflow(user_session) ``` This small program shows the whole shape in a short span: `Session` carries the data, `Sandbox` places execution, `Tool` is registered on an `Agent`, `Memory` persists across runs, `Workflow` composes the path, and `Compressor` shrinks the resulting context. --- ## The Runtime Pieces ### Session — The Flowing State `Session` is OpenRath's central runtime value. It holds an ordered chunk table containing system, user, assistant, and tool-result rows. It also records sandbox placement, lineage, token usage, and pending lazy work. This is why OpenRath can model more than a single chat transcript. A session can be forked into a new branch, detached from its parent chain, merged back with another compatible branch, serialized to JSONL, or handed to a different workflow. Agent-side instructions are represented as their own session chunks and prepended by the loop, rather than repeatedly pasted into an opaque prompt. Common entry points: - `Session.from_user_message(...)` creates a user-side session. - `Session.from_agent_prompt(...)` creates an agent/system prompt session. - `session.to("local", spec="./")` binds a sandbox backend and workspace. - `session.fork()` creates a traceable branch. - `session.detach()` creates a new session without parent lineage. - `session.merge(...)` combines compatible branches and records merge lineage. ### Sandbox — Where Tools Land Sandbox is the execution placement for a session. It is the OpenRath equivalent of deciding where computation lands. Tools do not run in an abstract prompt; they run against the sandbox currently bound to the session. ```python session = Session.from_user_message("List files").to("local", spec="./") ``` The `local` backend is always available and runs file, command, and code tools against the host workspace. The optional `opensandbox` backend connects the same tool layer to a containerized OpenSandbox runtime. A returned session keeps the active sandbox ownership, so later tool calls continue from the same execution context instead of silently drifting to another directory or machine. ### Memory — What Survives Between Runs Memory is a persistent plane parallel to sandbox execution. It is not a tool result and not just prompt text; it is state that can be attached to an agent, recalled before a run, and committed after a run. The base install includes a zero-dependency local memory backend. It stores data under `.openrath/memory/`, supports lexical BM25 recall without an LLM, and can use embeddings when an embedding provider is configured. OpenViking is available as an optional backend for users who want a richer external memory service. ```python with flow.Agent("You remember useful facts.", model="gpt-5.5", memory="local") as agent: agent.remember_memory("The user works mostly in Python.") hits = agent.recall_memory("preferred programming language") ``` Agent memory APIs are intentionally discoverable: - `memory=` binds a store at construction time. - `remember_memory(...)` writes explicit facts. - `recall_memory(...)` retrieves relevant entries. - `commit_memory(...)` stores a transcript after a run. - `commit_on_forward=True` can commit automatically. ### Tool — Callable Functions for the Model `FlowToolCall` is the model-visible tool abstraction. It owns both sides of a tool: the name, description, and JSON schema shown to the LLM, plus the Python call that executes against a `Session`. This keeps tool schema and tool behavior together. Built-in tools cover common filesystem, shell, and code execution paths. Custom Python tools can implement the same interface, and stdio MCP tools can be adapted into the loop as normal `FlowToolCall` instances. The important split is: - `FlowToolCall` is the flow-layer function visible to the model. - `BackendTool*` is the lower-level payload consumed by a sandbox backend. ### Agent — One Reusable Layer `flow.Agent` is the small reusable layer most users start with. It is closer to `nn.Linear` than to a full application: it has a prompt, a provider, optional tools, optional memory, and a `forward(session) -> session` path. An agent does not own the whole world. The session loop remains the engine, the sandbox remains session placement, and memory remains a separate store. This keeps the single-agent case simple while still allowing the same agent to appear inside larger workflows. ### Workflow — Compose Without Chaos `flow.Workflow` is the composition surface. Subclasses implement: ```python def forward(self, session: Session) -> Session: ... ``` A workflow can chain agents, fork sessions, compress context, call tools, dispatch to child workflows, and return a new session. Because the input and output are both `Session`, workflows can be nested without inventing a new state format at each layer. For routing that depends on the conversation at runtime, `flow.Selector` is an LLM-backed router over self-describing workflows (each carries a `description`). It returns the next workflow to run, or a no-op `flow.EmptyWorkflow` when the task is done — so `if` / `while` stay plain Python: ```python selector = flow.Selector(provider) while not isinstance( nxt := selector.forward(session, triage, tech, wrapup), flow.EmptyWorkflow ): session = nxt(session) ``` --- ## Quick Install ```bash pip install openrath ``` Optional sandbox and memory integrations: ```bash pip install "openrath[opensandbox]" pip install "openrath[openviking]" ``` For source development: ```bash git clone https://github.com/Rath-Team/OpenRath.git cd OpenRath uv sync --group dev --group docs ``` Most LLM examples use OpenAI-compatible environment variables: ```bash export OPENAI_API_KEY=sk-... export OPENAI_BASE_URL=https://your-gateway/v1 export OPENAI_DEFAULT_MODEL=your-model-name ``` You can also configure providers in `~/.openrath/config.json`. Environment variables take precedence. --- ## Learn By Running The `example/` directory is a numbered learning ladder. Each script introduces one concept, keeps boilerplate in `_shared/`, and shows how the core objects fit together. Run the first rung: ```bash python example/01_hello_agent.py ``` | # | File | Concept | Needs a key? | | --- | --- | --- | :---: | | 01 | [`01_hello_agent.py`](example/01_hello_agent.py) | The smallest OpenRath program: build `flow.Agent`, call it on `Session`, stream a response. | yes | | 02 | [`02_session_lineage.py`](example/02_session_lineage.py) | Branch a session with `fork`, cut lineage with `detach`, inspect the session graph, export JSONL. | no | | 03 | [`03_sandbox_backend.py`](example/03_sandbox_backend.py) | Place the same session on `local` or `opensandbox` and observe where tools execute. | yes | | 04 | [`04_tools_builtin.py`](example/04_tools_builtin.py) | Use built-in filesystem and shell tools that every loop can expose. | yes | | 05 | [`05_custom_tool.py`](example/05_custom_tool.py) | Implement a custom `FlowToolCall` with a JSON schema and Python runtime behavior. | yes | | 06 | [`06_mcp_tool.py`](example/06_mcp_tool.py) | Wrap a tiny stdio MCP server and borrow its tools without writing new tool classes. | no | | 07 | [`07_streaming.py`](example/07_streaming.py) | Receive streaming deltas and inspect cumulative token usage after the run. | yes | | 08 | [`08_compress.py`](example/08_compress.py) | Use `flow.Compressor` to reduce a long session into a smaller context session. | yes | | 09 | [`09_memory.py`](example/09_memory.py) | Use the local memory backend to remember, recall, and optionally commit a live turn. | no | | 10 | [`10_provider_variation.py`](example/10_provider_variation.py) | Swap model vendors by changing `Provider`, while keeping Session and Workflow code stable. | yes | | 11 | [`11_dynamic_selector.py`](example/11_dynamic_selector.py) | Route between self-describing workflows with `flow.Selector`: `if` branching and a `while` loop that ends on `flow.EmptyWorkflow`. | yes | | 12 | [`12_compile.py`](example/12_compile.py) | Statically `compile()` a workflow: inspect its resource manifest, run offline `validate()`, and use the lifecycle context manager. | no | Read [`example/README.md`](example/README.md) for setup details and shared helpers. For complete, production-shaped scenarios with fixed inputs, durable state, QA, and committed deliverables, see [`Rath-Team/OpenRath-Example`](https://github.com/Rath-Team/OpenRath-Example). --- ## Docs and Links - Docs: [https://docs.openrath.com](https://docs.openrath.com) - Repository: [https://github.com/Rath-Team/OpenRath](https://github.com/Rath-Team/OpenRath) - Issues: [https://github.com/Rath-Team/OpenRath/issues](https://github.com/Rath-Team/OpenRath/issues) Build docs locally: ```bash uv run sphinx-build -M html docs/source docs/_build ``` --- ## License OpenRath uses a BSD-style license. See [LICENSE](LICENSE).