Skip to main content

Agent Substrate

A production-ready, protocol-oriented Python framework for building robust, observable, and composable autonomous AI agents and durable multi-agent workflows.

Python 3.13+ License: MIT Docs

The chat app built on Agent Substrate: streaming answers, scheduled tasks, approvals, notifications and settings
The chat app that ships with it (substrate-ui) running on this engine.


🚀 Features

  • 🤖 ReAct & Multi-Agent Loops: Production-grade Reasoning + Action loop (ReActAgent), hub-and-spoke sub-agent delegation (OrchestratorAgent), human-in-the-loop gates (DurableApproval), supervision token budgets (SpawnBudget, ExecutionBudget), and priority preemption.
  • 🔧 Type-Safe Tool System: @tool(risk=ToolRisk.SAFE, idempotent=True) decorator with automated schema reflection, risk-tiered approval gating (ToolRisk), MCP client integration, and sandboxed code execution.
  • 💾 Unified Storage (substrate.stores): Single Store port (connect("./.substrate") or postgres_store) managing:
    • Threads: DAG-based conversation history with branching, forking, and compaction checkpoints.
    • Vectors: Dense similarity, lexical word matching, and hybrid RRF fusion (SearchableVectorStore).
    • Memory: Key-value session state (short-term) and semantic fact extraction (long-term).
    • Tasks: Per-agent Kanban boards with 6-state lifecycle tracking.
    • Knowledge Graph: Entity/relationship graph store with Cypher query capability.
    • Files: Content-addressed blob and workspace file store with quota management.
  • 📄 Layout-Aware Document Intelligence (substrate.documents): Reader parses PDF, DOCX, PPTX, XLSX, ODF, HTML, Markdown, and CSV into structured markdown. Files into an OKF Library where models navigate documents using DocumentsTool (outline, read, find, view) without context dumping.
  • 🛡️ 13 Built-in Middlewares & Safety: Turn-key guardrails covering PII detection, prompt-injection defense with UTS-39 homoglyph normalization, token limiters, caching, schema validation, rate limiters, and LLM-as-a-judge.
  • 📡 Deterministic Durability & Replay: Hierarchical effect journaling (Effect.make_id) guaranteeing at-most-once side-effects on replay, lease-based worker scheduling, and verified crash recovery.
  • 🎯 Multi-Provider LLMs: Native support for OpenAI, Anthropic, Gemini, Groq, and Ollama via LLMFactory and create_model_client.
  • 🕷️ Composable Flows: Pure coordination primitives (SequentialFlow, ParallelFlow, ConditionalFlow) for deterministic agent pipelines.
  • 📊 Zero-Overhead Observability: Built-in OpenTelemetry instrumentation (opentelemetry-api with zero runtime overhead), structured logging, and Grafana / Tempo integration.
  • 🌐 HTTP & SSE Serving (substrate.server): Turn-key FastAPI integration (create_app) with AG-UI wire protocol and Server-Sent Events (SSE).

📋 Table of Contents


⚡ Quick Start

Installation

Add it to your project using uv (recommended) or pip:

uv add agent-substrate
# or
pip install agent-substrate

The base install contains the complete engine (pydantic, typing_extensions, opentelemetry-api, pypdfium2, and confusable_homoglyphs) and stores everything in an embedded SQLite folder, so it runs with no other service. Optional integrations (vendor LLMs, Postgres, Redis, governed files, OCR) are extras; see Tech Stack:

uv add "agent-substrate[openai,postgres]"

Your First Agent

Runtime.run(agent, prompt) is the one-shot entry point: it registers the agent, executes the prompt, and returns a RunOutcome with the final answer.

import asyncio
from substrate import ReActAgent, Runtime
from substrate.integrations.llm import LLMFactory

async def main():
    llm = LLMFactory("gpt-4o", api_key="sk-...").build()

    agent = ReActAgent(
        "assistant",
        model=llm,
        system_instructions="You are a helpful assistant.",
    )

    async with Runtime.open("./.substrate") as runtime:
        result = await runtime.run(agent, "Write a Python function to compute Fibonacci numbers.")
        print(result.output)

if __name__ == "__main__":
    asyncio.run(main())

Agent with Tools

You can define tools using the @tool decorator or reusable tool classes:

import asyncio
from substrate import ReActAgent, Runtime, ToolRisk, tool
from substrate.integrations.llm import LLMFactory
from substrate.integrations.tools.compute.calculator import CalculatorTool

@tool(risk=ToolRisk.SAFE, idempotent=True)
def shout(text: str) -> str:
    """Upper-case the text."""
    return text.upper()

async def main():
    llm = LLMFactory("gpt-4o", api_key="sk-...").build()

    agent = ReActAgent(
        "math_expert",
        model=llm,
        tools=[shout, CalculatorTool()],
        system_instructions="Answer math queries using the calculator.",
    )

    async with Runtime.open("./.substrate") as runtime:
        result = await runtime.run(agent, "Calculate 1234 * 5678 and shout the result.")
        print(result.output)

if __name__ == "__main__":
    asyncio.run(main())

🧱 Tech Stack: required vs opt-in

Two things are called "Agent Substrate": the library (pip install agent-substrate, an engine you embed) and the platform (agents/substrate-cloud, a multi-tenant HTTP server built on it, which the chat app talks to). They need different things.

The library needs nothing but Python

Required Python 3.13+, pydantic, typing_extensions, opentelemetry-api, pypdfium2, confusable_homoglyphs.
State One embedded SQLite database plus a files folder (Runtime.open("./.substrate")): runs and their journal, threads, memory, tasks, graph, vectors, files. No Postgres, no Redis, no vector database to install.
Documents PDF through PDFium, and DOCX/PPTX/XLSX/ODF/HTML/Markdown/CSV with the standard library, all in the base install. Scanned pages: the tesseract program if it is on your PATH, otherwise they are reported in needs_ocr, never returned silently empty.
Search Words (SQLite full-text) out of the box. Meaning (embeddings) only when you give a Library an embedder.

Everything below is an extra you choose (uv add "agent-substrate[openai,postgres]"); nothing imports until you use it.

Opt in when you want… Extra / service Adds
A model provider openai (also vLLM, llama.cpp, Ollama, LM Studio), anthropic, gemini that vendor's SDK
PostgreSQL instead of SQLite (several workers or hosts) postgres asyncpg; vectors use the pgvector extension
Redis (session cache, event bus) redis redis client
Governed, versioned files through a Substrate Data service instead of local disk data substrate-data-client (httpx only; bytes go straight to object storage on signed URLs)
Scheduled / cron tasks scheduler apscheduler
OCR for scanned pages without a system program ocr RapidOCR: PaddleOCR's own small models on ONNX Runtime, inside the wheel (~120-150 MB)
Layout, tables and chart reading the document-intelligence service (make infra-up-document-intelligence) PaddleOCR-VL in a CPU container (~1 GB image), called over HTTP
Search by meaning an embedding model, or the embedding-reranker service (make infra-up-embedding-reranker) Qwen3-VL embedding and reranking
Prompt-injection / NSFW guardrails safety ONNX classifiers (onnxruntime, tokenizers)
Web search and browsing web Playwright, crawl4ai, Tavily/Exa clients (hundreds of MB)
Code execution sandbox (data-science packages) and an nsjail or Kubernetes (code) runtime
MCP tools, TTS, the CLI console mcp, tts (Kokoro), console
An HTTP/SSE server around an agent serve fastapi

Full reasoning per extra (sizes, pins, exclusivity) is in optional-dependencies.md.

No LanceDB. Vectors live in the same database as everything else (SQLite, or PostgreSQL with pgvector). The "Lance database" comment in stores/store.py is only an analogy for how the folder is laid out.

The platform (agents/substrate-cloud) is a deployment, so it needs services

Needed Why
PostgreSQL 18 (with pgvector) The application tables (users, threads, files, scheduled tasks, notifications, preferences) use JSONB and row-level security, and the engine store runs on it (STORE_BACKEND=postgres). STORE_BACKEND=local keeps the engine state in a folder, but the application tables still need Postgres.
Redis 7 Rate limits and daily quotas, the session cache, the event bus.
An LLM key Any one provider above.
Docker Only to run the two services above with make infra-up.

Optional on the platform, each off until configured: Substrate Data for files (DATA_SERVICE_URL; without it files are a local folder), the document-intelligence and embedding-reranker services, ONLYOFFICE for editing Office files server-side, Resend for emailing scheduled-task results (RESEND_API_KEY), and Grafana, Loki and Tempo for observability.

The apps around it

App Stack
substrate-ui (chat) Next.js 16, React, TypeScript, Tailwind v4, Radix UI, pnpm.
agent-substrate-platform (control plane) Next.js 16, Prisma on PostgreSQL, Auth.js, Tailwind v4, pnpm.

🏛️ Core Architecture

Agent Substrate is organized into 13 strictly ordered concept layers (tests/_layout.py). Imports flow strictly downward — a concept can only import from layers below it:

agents         ← ReActAgent, OrchestratorAgent, UserProxyAgent, Flows (Sequential, Parallel, Conditional)
  runtime        ← Runtime, Worker, Scheduler, Journal, Commit, Effect log
    middleware   ← 13 built-in middlewares (PII, Prompt Injection, Cache, Retry, Truncation, LLM Judge)
      context      ← Context window management, sliding-window compaction, token budgeting
        safety       ← Input safety classifiers, UTS-39 homoglyph normalization
          workspace    ← Content-addressed workspace filesystems, branching, snapshots
            documents    ← Layout-aware Reader, OKF Library, DocumentsTool
              stores       ← Store facade, SQLite WAL database, Threads, Vectors, Memory, Tasks, Graphs
                models       ← ChatModel / EmbeddingModel protocols, capabilities registry, error classification
                  tools        ← @tool decorator, Tool protocol, approval gates, chains, skills
                    telemetry    ← OpenTelemetry spans and metrics (API only, zero runtime overhead)
                      types        ← ContentBlock union, Message envelope, Actor/Topic IDs, Errors, Usage

Outside the Core:

  • substrate.integrations: Out-of-tree adapters for third-party vendors (OpenAI, Anthropic, Gemini, Groq, Ollama), database drivers (PostgresDatabase, postgres_store), governed files (DataFileStore), Redis, and sandboxes (Docker / nsjail).
  • substrate.server: Turn-key FastAPI HTTP and Server-Sent Events (SSE) serving layer with AG-UI wire protocol support.
  • tests/invariants: An executable invariant register with 221 property tests verifying crash survival, replay determinism, and zero vendor leak.

🔑 Key Patterns

Writing Tools

Function Decorator

from substrate import tool, ToolRisk

@tool(risk=ToolRisk.SAFE, idempotent=True)
def get_user(user_id: str) -> dict:
    """Look up a user account by ID."""
    return {"id": user_id, "name": "Alice"}

Class-Based Tool

from substrate.tools import ToolExecutionResult
from substrate.types.content import TextBlock

class CustomTool:
    name = "custom_tool"
    description = "Perform a custom operation."
    input_schema = {
        "type": "object",
        "properties": {"query": {"type": "string"}},
        "required": ["query"],
    }

    async def execute(self, *, ctx=None, **kwargs) -> ToolExecutionResult:
        return ToolExecutionResult(content=[TextBlock(text="Success")])

Human-in-the-Loop (HITL) Approvals

Risky tools pause execution durably without blocking the worker process:

from substrate.tools import DurableApproval, ApprovalDecision, tool, ToolRisk

approval = DurableApproval()

@tool(risk=ToolRisk.HIGH)
async def transfer_funds(account: str, amount: float) -> str:
    """Transfer funds to another account."""
    return f"Transferred ${amount} to {account}"

# In your runtime or HTTP handler, resolve pending decisions:
# await approval.resolve(request_id, ApprovalDecision.APPROVE)

💾 Storage & Persistence

The entire state of an agent (threads, memory, vector embeddings, tasks, and files) is encapsulated in a single Store:

from substrate import Store, connect

# Local embedded database (SQLite WAL + files directory):
store = await connect("./.substrate")

# Scale out across multiple worker processes / hosts:
from substrate.integrations.database import postgres_store
store = await postgres_store("postgresql://user:pass@localhost:5432/agentdb")

Store Facets

  • store.threads: DAG-based conversation threads with branching, forking, and history checkpoints.
  • store.vectors: Multimodal vector storage supporting exact, lexical, and hybrid Reciprocal Rank Fusion (RRF) search.
  • store.memory: Long-term episodic memory with full-text search + short-term session state.
  • store.tasks: Scoped Kanban task boards with automatic retry tracking.
  • store.graph: Knowledge graph entities and relationships with Cypher query support.
  • store.files: Content-addressed file store with per-tenant quotas.

📄 Document Intelligence

Read, file, and navigate documents without flooding the LLM context window:

from substrate.documents import Reader, Library, DocumentsTool
from substrate.stores import Store

# Read PDF, DOCX, PPTX, XLSX, ODF, HTML, Markdown, CSV:
result = await Reader().read(data, "report.pdf")

store = Store.at("./.substrate")
library = Library(store)
await library.add(data, "report.pdf", collection="conversations/c1/documents")

# Give the agent a tool to navigate the library (list, outline, read, find, view):
doc_tool = DocumentsTool(library, collection=lambda scope: "conversations/c1/documents")

🕸️ Multi-Agent Workflows

OrchestratorAgent — Hub & Spoke

OrchestratorAgent coordinates specialist sub-agents using dynamic tool delegation:

from substrate import OrchestratorAgent, SubAgentConfig, ReActAgent, Runtime

researcher = ReActAgent("researcher", model=llm, system_instructions="Research topics.")
writer = ReActAgent("writer", model=llm, system_instructions="Draft articles.")

orchestrator = OrchestratorAgent(
    "coordinator",
    model=llm,
    sub_agents=[
        SubAgentConfig(agent=researcher, description="Research technical topics"),
        SubAgentConfig(agent=writer, description="Write clear blog posts"),
    ],
)

async with Runtime.open("./.substrate") as runtime:
    result = await runtime.run(orchestrator, "Research and draft an article on Rust vs Go.")
    print(result.output)

Deterministic Flows

Compose pipelines using SequentialFlow, ParallelFlow, and ConditionalFlow:

from substrate.agents import SequentialFlow
from substrate.runtime import Runtime
from substrate.types import Actor

class FetchStep:
    id = Actor(type="agent", key="fetch")
    async def run(self, ctx, inbox):
        for msg in inbox:
            await ctx.reply(msg, {"text": "Fetched 3 records."})

class AnalyzeStep:
    id = Actor(type="agent", key="analyze")
    async def run(self, ctx, inbox):
        for msg in inbox:
            await ctx.reply(msg, {"text": "Analysis: all records valid."})

async def main():
    fetch, analyze = FetchStep(), AnalyzeStep()
    pipeline = SequentialFlow(steps=[fetch, analyze], name="demo_pipeline")

    async with Runtime.open("./.substrate") as runtime:
        await runtime.register(fetch)
        await runtime.register(analyze)
        result = await runtime.ask(pipeline, "Process the latest dataset.")
        print(result.output)

🧪 Testing

# Run unit & integration test suites
uv run pytest

# Run the 221-test Invariant Register (durability, replay determinism, import boundaries)
uv run pytest tests/invariants/

# Run README and example suites
uv run pytest tests/test_readme_examples.py tests/test_examples.py

# Check import boundaries and concept layering
uv run lint-imports

📖 Documentation


Built with ❤️ for the AI agent engineering community

Metadata

Release files for agent-substrate 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for agent-substrate 0.4.0
File Size Uploaded Provenance
agent_substrate-0.4.0.tar.gz 703.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for agent-substrate 0.4.0
File Interpreter ABI Platform
agent_substrate-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.6 MB

Release files / agent_substrate-0.4.0.tar.gz

Download URL agent_substrate-0.4.0.tar.gz
Size 703.2 kB
Tags Source
SHA-256 checksum
How to use checksums
0adea5a3c52378d5376a154d8b9b421476ca704fcab11a7f8a970d0a6f488f76
BLAKE2b-256 checksum
How to use checksums
3897647a77c87fa6db318c551e0db67d41874237a40c6f61473e5c26c6b04f7e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / agent_substrate-0.4.0-py3-none-any.whl

Download URL agent_substrate-0.4.0-py3-none-any.whl
Size 873.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b1a11252ad23f782d2c8588538ee9e6968d7f94ef2fe34746cd28277299cadb1
BLAKE2b-256 checksum
How to use checksums
0685643fc284679cb7e3f52a532c30312d7b02e21f1c1200bd44739a71e77d84
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for agent-substrate 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page