Deterministic orchestration layer for MCP-based agents.
Project description
ChainWeaver
Deterministic orchestration layer for MCP-based agents.
Compiled execution graphs, not interpreted reasoning.
The Problem
When an LLM-powered agent chains tools together — fetch_data → transform → store — a
common pattern is to insert an LLM call between every step so the model can "decide"
what to do next.
User request
│
▼
LLM call ──► Tool A
│
▼
LLM call ──► Tool B
│
▼
LLM call ──► Tool C
│
▼
Response
For chains that are fully deterministic (the next step is always the same given the previous output) these intermediate LLM calls add:
- Latency — each round-trip costs hundreds of milliseconds.
- Cost — every call consumes tokens and credits.
- Unpredictability — a language model might route differently on each invocation.
The Solution
ChainWeaver compiles deterministic multi-tool chains into executable flows that run without any LLM involvement between steps.
User request
│
▼
FlowExecutor ──► Tool A ──► Tool B ──► Tool C
│
▼
Response
Think of it as the difference between an interpreter and a compiler:
| Naive agent chaining | ChainWeaver flow |
|---|---|
| Interpreted, step by step | Compiled, graph-first |
| LLM decides each next action | Execution is pre-wired |
| Non-deterministic by default | Deterministic by design |
| Cost scales with steps | Fixed overhead per flow invocation |
Quick Start
Installation
pip install chainweaver
Define tools, build a flow, and execute it
from pydantic import BaseModel
from chainweaver import Tool, Flow, FlowStep, FlowRegistry, FlowExecutor
# --- 1. Declare schemas ---
class NumberInput(BaseModel):
number: int
class ValueOutput(BaseModel):
value: int
class ValueInput(BaseModel):
value: int
class FormattedOutput(BaseModel):
result: str
# --- 2. Implement tool functions ---
def double_fn(inp: NumberInput) -> dict:
return {"value": inp.number * 2}
def add_ten_fn(inp: ValueInput) -> dict:
return {"value": inp.value + 10}
def format_result_fn(inp: ValueInput) -> dict:
return {"result": f"Final value: {inp.value}"}
# --- 3. Wrap as Tool objects ---
double_tool = Tool(
name="double",
description="Takes a number and returns its double.",
input_schema=NumberInput,
output_schema=ValueOutput,
fn=double_fn,
)
add_ten_tool = Tool(
name="add_ten",
description="Takes a value and returns value + 10.",
input_schema=ValueInput,
output_schema=ValueOutput,
fn=add_ten_fn,
)
format_tool = Tool(
name="format_result",
description="Formats a numeric value into a human-readable string.",
input_schema=ValueInput,
output_schema=FormattedOutput,
fn=format_result_fn,
)
# --- 4. Define the flow ---
flow = Flow(
name="double_add_format",
description="Doubles a number, adds 10, and formats the result.",
steps=[
FlowStep(tool_name="double", input_mapping={"number": "number"}),
FlowStep(tool_name="add_ten", input_mapping={"value": "value"}),
FlowStep(tool_name="format_result", input_mapping={"value": "value"}),
],
)
# --- 5. Execute ---
registry = FlowRegistry()
registry.register_flow(flow)
executor = FlowExecutor(registry=registry)
executor.register_tool(double_tool)
executor.register_tool(add_ten_tool)
executor.register_tool(format_tool)
result = executor.execute_flow("double_add_format", {"number": 5})
print(result.success) # True
print(result.final_output) # {'number': 5, 'value': 20, 'result': 'Final value: 20'}
for record in result.execution_log:
print(record.step_index, record.tool_name, record.outputs)
# 0 double {'value': 10}
# 1 add_ten {'value': 20}
# 2 format_result {'result': 'Final value: 20'}
You can also run the bundled example directly:
python examples/simple_linear_flow.py
Architecture
chainweaver/
├── __init__.py # Public API
├── tools.py # Tool — named callable with Pydantic schemas
├── flow.py # FlowStep + Flow — ordered step definitions
├── registry.py # FlowRegistry — in-memory flow catalogue
├── executor.py # FlowExecutor — deterministic, LLM-free runner
├── exceptions.py # Typed exceptions with traceable context
└── logging.py # Structured per-step logging
Core abstractions
Tool
Tool(
name="my_tool",
description="...",
input_schema=MyInputModel, # Pydantic BaseModel
output_schema=MyOutputModel, # Pydantic BaseModel
fn=my_callable,
)
A tool wraps a plain Python callable together with Pydantic models for strict input/output validation.
FlowStep
FlowStep(
tool_name="my_tool",
input_mapping={"key_for_tool": "key_from_context"},
)
Maps keys from the accumulated execution context into the tool's input schema. String values are looked up in the context; non-string values are treated as literal constants.
Flow
Flow(
name="my_flow",
description="...",
steps=[step_a, step_b, step_c],
deterministic=True, # enforced by design
trigger_conditions={"intent": "process data"}, # optional metadata
)
An ordered sequence of steps.
FlowRegistry
registry = FlowRegistry()
registry.register_flow(flow)
registry.get_flow("my_flow")
registry.list_flows()
registry.match_flow_by_intent("process data") # basic substring match
An in-memory catalogue of flows.
FlowExecutor
executor = FlowExecutor(registry=registry)
executor.register_tool(tool_a)
result = executor.execute_flow("my_flow", {"key": "value"})
Runs a flow step-by-step with full schema validation and structured logging. No LLM calls are made at any point.
Data flow
initial_input (dict)
│
▼
┌─────────────────────────────────────────────┐
│ Execution context (cumulative dict) │
│ │
│ Step 0: resolve inputs → run tool → merge │
│ Step 1: resolve inputs → run tool → merge │
│ Step N: resolve inputs → run tool → merge │
└─────────────────────────────────────────────┘
│
▼
ExecutionResult.final_output (merged context)
MCP Integration Concept
ChainWeaver is designed to sit between an MCP server and your agent loop:
MCP Agent
│ (observes tool call sequence at runtime)
▼
ChainWeaver FlowRegistry
│ (matches pattern → retrieves compiled flow)
▼
FlowExecutor
│ (runs deterministic steps without LLM involvement)
▼
MCP Tool Results
In practice:
- An agent calls
tool_a, thentool_b, thentool_cseveral times with the same routing logic. - A higher-level observer detects the pattern and registers a named
Flow. - On subsequent invocations the executor runs the entire chain in a single call — no intermediate LLM calls required.
Error Handling
All errors are typed and traceable:
| Exception | When it is raised |
|---|---|
ToolNotFoundError |
A step references an unregistered tool |
FlowNotFoundError |
The requested flow is not registered |
FlowAlreadyExistsError |
Registering a flow that already exists (without overwrite=True) |
SchemaValidationError |
Input or output fails Pydantic validation |
InputMappingError |
A mapping key is not present in the context |
FlowExecutionError |
The tool callable raises an unexpected exception |
All exceptions inherit from ChainWeaverError.
Roadmap
v0.1 — MVP (current)
-
Toolwith Pydantic input/output schemas -
Flowas an ordered list ofFlowStepobjects -
FlowRegistry(in-memory) -
FlowExecutor(sequential, LLM-free) - Structured per-step logging
- Typed exceptions
- Full test suite
v0.2 — DAG & Branching
- DAG-based execution with dependency edges
- Parallel step groups
- Conditional branching inside flows
v0.3 — Persistence & Learning
- JSON/YAML flow storage and reload
- Runtime chain observation (record ad-hoc tool sequences)
- Automatic flow suggestion from observed patterns
v0.4 — Scoring & Observability
- Determinism scoring for partial flows
- OpenTelemetry trace export
- Async execution mode
Comparison: Naive Agent Chaining vs ChainWeaver
| Criterion | Naive LLM chaining | ChainWeaver |
|---|---|---|
| LLM calls per step | 1 per step | 0 |
| Latency | O(n × LLM RTT) | O(n × tool RTT) |
| Cost | O(n × token cost) | Fixed infra cost |
| Reproducibility | Non-deterministic | Deterministic |
| Schema validation | Ad-hoc / none | Pydantic enforced |
| Observability | Prompt logs only | Structured step logs |
| Reusability | Prompt templates | Registered, versioned flows |
Development
# Install with dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run the example
python examples/simple_linear_flow.py
License
MIT — 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
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 chainweaver-0.0.1.tar.gz.
File metadata
- Download URL: chainweaver-0.0.1.tar.gz
- Upload date:
- Size: 27.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e88a157b77c0a13ede1c315f51aa065a4868688cf4a3eb770e4c556e86f56288
|
|
| MD5 |
562f14d98e61b073a0d86bc041f94171
|
|
| BLAKE2b-256 |
bac1b24c107ca8b31f7c9d086afe99f585fe1852b56c703fe5030960858d3a32
|
Provenance
The following attestation bundles were made for chainweaver-0.0.1.tar.gz:
Publisher:
publish.yml on dgenio/ChainWeaver
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
chainweaver-0.0.1.tar.gz -
Subject digest:
e88a157b77c0a13ede1c315f51aa065a4868688cf4a3eb770e4c556e86f56288 - Sigstore transparency entry: 1017800155
- Sigstore integration time:
-
Permalink:
dgenio/ChainWeaver@1b524159c161722bb4d0ccdefe3fbebb5fc35592 -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/dgenio
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1b524159c161722bb4d0ccdefe3fbebb5fc35592 -
Trigger Event:
push
-
Statement type:
File details
Details for the file chainweaver-0.0.1-py3-none-any.whl.
File metadata
- Download URL: chainweaver-0.0.1-py3-none-any.whl
- Upload date:
- Size: 22.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9ffbc5c250b4d4eb65f8eae72f12904aed3312e635b3734b24f58ea5f32e9595
|
|
| MD5 |
959de0b1e0fdb95a5fc3c8778f22c0af
|
|
| BLAKE2b-256 |
9c8f89fc59afefdbf47999d5aa75a18e0c31e8e7474d0a0d6cc24dc574cbcbde
|
Provenance
The following attestation bundles were made for chainweaver-0.0.1-py3-none-any.whl:
Publisher:
publish.yml on dgenio/ChainWeaver
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
chainweaver-0.0.1-py3-none-any.whl -
Subject digest:
9ffbc5c250b4d4eb65f8eae72f12904aed3312e635b3734b24f58ea5f32e9595 - Sigstore transparency entry: 1017800178
- Sigstore integration time:
-
Permalink:
dgenio/ChainWeaver@1b524159c161722bb4d0ccdefe3fbebb5fc35592 -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/dgenio
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1b524159c161722bb4d0ccdefe3fbebb5fc35592 -
Trigger Event:
push
-
Statement type: