Skip to main content

The production runtime for Python agents

Project description

Civitas

The production runtime for Python agents.


Why Civitas?

Civitas is the Latin word for city-state — the community of citizens bound by shared laws, common purpose, and mutual protection.

The root is civis (citizen). It gives English: civilization, civic, civil, citizen. A civitas wasn't just a place — it was a self-governing body that conferred rights, identity, and protection on those who belonged to it.

Before Civitas, agents were isolated processes: no persistent identity, no rights, no protection. If one crashed, nothing noticed. Nothing restarted it.

Civitas is the covenant that changes what they are. It gives agents citizenship: a runtime that watches over them, restarts them on failure, routes messages between them, and traces every action — automatically.


Civitas brings Erlang's battle-tested fault-tolerance model to Python agent systems: supervision trees that restart crashed agents automatically, transport-agnostic message passing that scales from a single script to a distributed cluster, and first-class observability with zero instrumentation code.

pip install civitas

The problem

Most Python agent frameworks are great at calling LLMs. They are not runtimes. When an agent crashes, nothing restarts it. When a tool hangs, nothing times it out. When you need to scale across machines, you rewrite the routing layer. When something goes wrong in production, you have a log file.

Civitas is the infrastructure layer underneath your agent code. It handles the hard parts — process lifecycle, fault tolerance, message routing, and distributed tracing — so your agents can focus on reasoning.


Quickstart

A supervised agent that crashes occasionally and recovers automatically:

import asyncio
import random
from civitas import AgentProcess, Runtime, Supervisor
from civitas.messages import Message

class FlakyWorker(AgentProcess):
    async def handle(self, message: Message) -> Message | None:
        if random.random() < 0.4:
            raise RuntimeError("transient failure")          # crashes ~40% of the time
        return self.reply({"result": f"processed: {message.payload['task']}"})

async def main():
    runtime = Runtime(
        supervisor=Supervisor(
            "root",
            strategy="ONE_FOR_ONE",   # restart only the crashed child
            max_restarts=5,
            backoff="EXPONENTIAL",
            children=[FlakyWorker("worker")],
        )
    )
    await runtime.start()

    for i in range(10):
        result = await runtime.ask("worker", {"task": f"job-{i}"})
        print(result.payload["result"])   # always succeeds — supervisor handles the rest

    await runtime.stop()

asyncio.run(main())

The supervisor detects every crash, applies exponential backoff, and restarts the agent. Your call site never changes.


Core concepts

graph TD
    R[Runtime] --> S[Supervisor]
    S -->|ONE_FOR_ONE| A1[AgentProcess]
    S -->|ONE_FOR_ONE| A2[AgentProcess]
    S --> CS[Child Supervisor]
    CS -->|ONE_FOR_ALL| A3[AgentProcess]
    CS -->|ONE_FOR_ALL| A4[AgentProcess]

    A1 <-->|MessageBus| A2
    A2 <-->|MessageBus| A3
    A3 <-->|MessageBus| A4

    A1 --- LLM[ModelProvider]
    A2 --- Tools[ToolRegistry]
    A1 --- OTEL[OTEL Tracer]

    style R fill:#1e3a5f,color:#fff
    style S fill:#1e3a5f,color:#fff
    style CS fill:#1e3a5f,color:#fff
    style A1 fill:#2d6a4f,color:#fff
    style A2 fill:#2d6a4f,color:#fff
    style A3 fill:#2d6a4f,color:#fff
    style A4 fill:#2d6a4f,color:#fff

AgentProcess — the unit of computation. Subclass it, override handle(). Each process has its own mailbox, state, lifecycle hooks, and error boundary.

Supervisor — monitors child processes and applies restart strategies when they crash. Supervisors nest, forming a tree. Failures propagate upward only when a supervisor exhausts its restart budget.

MessageBus — routes typed messages between agents by name. Supports fire-and-forget (send), request-reply (ask), and glob-pattern broadcast.

Transport — the delivery layer underneath the bus. Swap it in the config, not in your agent code.


Multi-agent example

Three agents forming a research pipeline. Each runs in its own supervised process:

from civitas import AgentProcess, Runtime, Supervisor
from civitas.messages import Message

class Orchestrator(AgentProcess):
    async def handle(self, message: Message) -> Message | None:
        # fan out to researcher, collect, then summarize
        findings = await self.ask("researcher", {"query": message.payload["query"]})
        report   = await self.ask("summarizer", {"findings": findings.payload})
        return self.reply(report.payload)

class Researcher(AgentProcess):
    async def handle(self, message: Message) -> Message | None:
        result = await self.tools.get("web_search").execute(query=message.payload["query"])
        return self.reply({"findings": result})

class Summarizer(AgentProcess):
    async def handle(self, message: Message) -> Message | None:
        response = await self.llm.chat(
            model="claude-haiku-4-5",
            messages=[{"role": "user", "content": str(message.payload["findings"])}],
        )
        return self.reply({"report": response.content})

runtime = Runtime(
    supervisor=Supervisor("root", strategy="ONE_FOR_ONE", children=[
        Orchestrator("orchestrator"),
        Researcher("researcher"),
        Summarizer("summarizer"),
    ]),
    model_provider=AnthropicProvider(),
    tool_registry=tools,
)

Scaling ladder

The same agent code runs at every level. The only thing that changes is the topology configuration:

graph LR
    L1["Level 1\nSingle process\nInProcessTransport\npip install civitas"]
    L2["Level 2\nMulti-process\nZMQTransport\npip install civitas[zmq]"]
    L3["Level 3\nDistributed\nNATSTransport\npip install civitas[nats]"]
    L4["Level 4\nContainerized\ncivitas deploy --target docker-compose"]

    L1 -->|"change transport in topology.yaml"| L2
    L2 -->|"change transport in topology.yaml"| L3
    L3 -->|"civitas deploy"| L4

    style L1 fill:#1e3a5f,color:#fff
    style L2 fill:#2d6a4f,color:#fff
    style L3 fill:#6b4c11,color:#fff
    style L4 fill:#4a1942,color:#fff
# topology.yaml — switch from in-process to distributed by changing one block
transport:
  type: nats           # was: in_process, then: zmq
  url: nats://localhost:4222

supervision:
  name: root
  strategy: ONE_FOR_ONE
  children:
    - agent: { name: orchestrator, type: myapp.Orchestrator }
    - agent: { name: researcher,   type: myapp.Researcher,   process: worker }
    - agent: { name: summarizer,   type: myapp.Summarizer,   process: worker }
civitas run --topology topology.yaml

Supervision strategies

When a child crashes, the supervisor applies one of three strategies:

graph TD
    subgraph ONE_FOR_ONE["ONE_FOR_ONE — restart only the crashed child"]
        direction LR
        S1[Supervisor] --> A["Agent A ✓"]
        S1 --> B["Agent B ✗ → ↻"]
        S1 --> C["Agent C ✓"]
    end

    subgraph ONE_FOR_ALL["ONE_FOR_ALL — restart all children"]
        direction LR
        S2[Supervisor] --> D["Agent D ↻"]
        S2 --> E["Agent E ✗ → ↻"]
        S2 --> F["Agent F ↻"]
    end

    subgraph REST_FOR_ONE["REST_FOR_ONE — restart crashed + all younger siblings"]
        direction LR
        S3[Supervisor] --> G["Agent G ✓"]
        S3 --> H["Agent H ✗ → ↻"]
        S3 --> I["Agent I ↻"]
    end

Supervisors nest. If a supervisor exhausts its restart budget, it escalates to its parent. Backoff policies — CONSTANT, LINEAR, EXPONENTIAL — are configured per supervisor.


Automatic observability

Every message, LLM call, and tool invocation generates an OTEL span — no instrumentation code required:

[10:00:00.123] orchestrator → researcher: research_query
[10:00:00.135] researcher: llm.chat(claude-haiku-4-5)  1520 → 430 tokens  $0.0089
[10:00:02.025] researcher: tool.invoke(web_search)  450ms  OK
[10:00:02.480] researcher → summarizer: summarize_request
[10:00:02.495] summarizer: llm.chat(claude-haiku-4-5)  890 → 210 tokens  $0.0003
[10:00:03.100] summarizer → orchestrator: reply  OK
─────────────────────────────────────────────────────
Total: 2.977s  |  2 LLM calls  |  $0.0092  |  0 errors

To export to Jaeger, Grafana, or any OTEL-compatible backend:

export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
python my_agent.py

Trace context propagates automatically across process and machine boundaries.


Hero demo

A research assistant with four supervised agents: an Orchestrator that fans out tasks, a WebResearcher that searches (with retry on transient failures), a Synthesizer, and a Writer.

# No API key needed — uses a mock LLM
python examples/research_assistant.py "Compare AI safety approaches"

# With a real LLM
export ANTHROPIC_API_KEY=sk-...
python examples/research_assistant.py "Compare AI safety approaches" --live

# With full distributed tracing
docker run -d -p 16686:16686 -p 4317:4317 jaegertracing/all-in-one
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
python examples/research_assistant.py "Compare AI safety approaches"
open http://localhost:16686

Install

pip install civitas                        # core runtime

# Model providers (pick one or more)
pip install civitas[anthropic]             # Anthropic Claude
pip install civitas[openai]                # OpenAI GPT-4o, o1, o3
pip install civitas[gemini]                # Google Gemini 2.0 / 1.5
pip install civitas[mistral]               # Mistral Large / Codestral
pip install civitas[litellm]               # 100+ models via LiteLLM

# Transports
pip install civitas[zmq]                   # ZMQ multi-process transport
pip install civitas[nats]                  # NATS distributed transport

# Observability
pip install civitas[otel]                  # OpenTelemetry SDK + OTLP exporter

# Typical dev setup
pip install civitas[anthropic,otel]

Requires Python 3.12+.


How Civitas fits in the stack

Civitas is a runtime, not a framework. LangGraph, CrewAI, and the OpenAI Agents SDK define how you build agents. Civitas is the infrastructure that keeps them alive.

┌──────────────────────────────────────────────┐
│  CONTEXT LAYER                               │
│  Prompts, memory, RAG, AGENTS.md             │
├──────────────────────────────────────────────┤
│  CONTROL LAYER                               │
│  Guardrails, HITL gates, cost limits         │
├──────────────────────────────────────────────┤
│  RUNTIME LAYER  ◄── Civitas lives here        │
│  Process lifecycle, fault tolerance,         │
│  message routing, observability, scaling     │
└──────────────────────────────────────────────┘

Civitas wraps LangGraph and OpenAI SDK agents natively:

from civitas.adapters.langgraph import LangGraphAgent

# Your existing LangGraph graph gains supervision,
# message routing, and OTEL tracing in 3 lines.
class MyLangGraphAgent(LangGraphAgent):
    graph = compiled_graph

Compared to alternatives

Civitas Temporal LangGraph CrewAI
Category Agent runtime Durable execution Agent orchestration Agent framework
Supervision trees
Restart strategies ONE_FOR_ONE, ONE_FOR_ALL, REST_FOR_ONE Retry policies (activity level)
Message passing First-class MessageBus Signals (limited) Graph edges (static)
Transport abstraction InProcess → ZMQ → NATS Worker polling In-process only In-process only
Agent identity Persistent named process Workflow instance Graph node Role object
OTEL tracing Automatic, zero code Manual Manual
LLM-native ModelProvider protocol Bring your own ChatModel built-in Built-in
License Apache 2.0 MIT Proprietary (LangSmith) MIT

Temporal excels at linear pipelines requiring durable execution. Civitas wins for multi-agent systems with dynamic routing and supervision hierarchies. They're complementary — Temporal can run inside a Civitas activity.

LangGraph is great for single-agent graphs with checkpointing and HITL gates. The LangGraphAgent adapter runs a compiled graph inside a Civitas process, giving it supervision and transport for free.


Examples

python examples/hello_agent.py           # simplest possible agent
python examples/supervised_agent.py      # crash + auto-restart
python examples/research_pipeline.py     # three-agent pipeline
python examples/self_sufficient_agent.py # agent with LLM and tools
python examples/observable_pipeline.py   # full OTEL tracing
python examples/supervision_tree.py      # nested supervision strategies
python examples/research_assistant.py    # four-agent hero demo
python examples/stateful_workflow.py     # state persistence across restarts

CLI

Civitas ships a full CLI for running, inspecting, and deploying agent systems.

civitas run          — start a topology (supervisor or worker process)
civitas topology     — validate, visualise, and diff topology files
civitas state        — inspect and manage persisted agent state
civitas deploy       — generate Docker Compose deployment artifacts
civitas version      — show the installed version

Run a topology:

# Start the supervisor process
civitas run --topology topology.yaml

# Start a worker process (for agents with process: worker in the topology)
civitas run --topology topology.yaml --process worker

# Override transport at runtime without editing the file
civitas run --topology topology.yaml --transport nats --nats-url nats://prod:4222

Validate before deploying (CI-safe — exits 1 on error):

civitas topology validate topology.yaml

Visualise the supervision tree:

civitas topology show topology.yaml
# root  ONE_FOR_ONE  restarts: 5/60s  backoff: exponential
# ├── orchestrator  myapp.Orchestrator
# ├── researcher    myapp.Researcher    @worker
# └── summarizer    myapp.Summarizer    @worker
#
# Transport   nats  nats://localhost:4222
# Plugins     anthropic  sqlite
# Topology    3 agents  ·  1 supervisors  ·  1 processes

Diff two topology files (e.g. staging vs production):

civitas topology diff topology.staging.yaml topology.prod.yaml
# Supervision
#   ~ /root/researcher/@class   myapp.StubResearcher → myapp.Researcher
# Transport
#   ~ transport/@type           in_process → nats
# 2 changed

Inspect persisted agent state:

civitas state list --db civitas_state.db
civitas state clear orchestrator --db civitas_state.db   # clear one agent
civitas state clear --force --db civitas_state.db        # clear all

Generate a Docker Compose stack from a topology:

civitas deploy docker-compose --topology topology.yaml --output ./deploy
# Generates: Dockerfile, docker-compose.yml, .env

cd deploy
echo "ANTHROPIC_API_KEY=sk-ant-..." >> .env
docker compose up --build

# Scale workers horizontally
docker compose up --scale worker-worker=3

Documentation

Getting Started Install, hello agent, first supervised system
Core Concepts AgentProcess, Supervisor, MessageBus, Transport
Supervision Strategies, backoff, escalation, heartbeats
Messaging send, ask, broadcast, backpressure, trace propagation
Transports InProcess → ZMQ → NATS, switching levels
Observability OTEL spans, console exporter, Jaeger setup
Plugins ModelProvider, ToolProvider, StateStore, custom plugins
Topology YAML Schema reference, CLI commands
Deployment Level 1–4 deployment ladder
Framework Adapters LangGraph, OpenAI SDK integration
Architecture Runtime internals, component wiring
FAQ Why not Temporal? Why not LangGraph? GIL concerns
Contributing Dev setup, test strategy, plugin authoring

License

Apache 2.0. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

civitas-0.1.0.tar.gz (393.5 kB view details)

Uploaded Source

Built Distribution

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

civitas-0.1.0-py3-none-any.whl (94.1 kB view details)

Uploaded Python 3

File details

Details for the file civitas-0.1.0.tar.gz.

File metadata

  • Download URL: civitas-0.1.0.tar.gz
  • Upload date:
  • Size: 393.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for civitas-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a6376b1a7f60a69a5ea0f05c32e24a2afec21744fd9f8ccbdde954066632ae18
MD5 a736b09cfe9d71564af9143e1d28f02b
BLAKE2b-256 3e996ee56703435dccd833fea3f199689db76cfdde4a83a0271b0381440ab5b0

See more details on using hashes here.

Provenance

The following attestation bundles were made for civitas-0.1.0.tar.gz:

Publisher: publish.yml on jerynmathew/python-civitas

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file civitas-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: civitas-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 94.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for civitas-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 804a19e655b0c87b3ec7c0e149ceeaa182ada598950c4120adc59760d9241444
MD5 c2d9b76ab273c4b8b6444f220e2ca541
BLAKE2b-256 71b6d655db2772ff05e09bf61d3b8402179e1ac4ca8cccbfd93f4d17c2721a00

See more details on using hashes here.

Provenance

The following attestation bundles were made for civitas-0.1.0-py3-none-any.whl:

Publisher: publish.yml on jerynmathew/python-civitas

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

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