Framework-agnostic schemas and validators for multi-agent turn routing plans
Project description
Framework-agnostic schemas and validators for multi-agent turn routing plans.
Routing plan contracts — not behavioral agent safety contracts.
Install · Quick start · Topologies · CLI · Design · Contributing
Table of contents
- Why PlanContract?
- How it works
- Install
- Quick start
- Topologies at a glance
- Validation that CI can gate on
- CLI
- Core concepts
- Project structure
- Roadmap
- Contributing & license
Why PlanContract?
Most agent frameworks give you orchestration — LangGraph nodes, crew runners, tool loops — but leave plan validation as an exercise. Teams rebuild the same logic in every codebase:
| Problem | PlanContract answer |
|---|---|
| LLM emits invalid agent IDs | PlanPolicy allowlists |
| Too many tasks per turn | configurable max_tasks |
| Duplicate or missing task IDs | structured ValidationReport |
Cycles in depends_on |
Kahn topological sort + DEPENDENCY_CYCLE |
Wrong parallel vs sequential label |
topology derived from the graph, not trusted from the LLM |
| Same agent scheduled twice | merge_same_agent_tasks() |
PlanContract is the contract between planner and dispatcher: typed schemas in, validated execution waves out.
Note: This is not agentcontract — that project covers behavioral/safety contracts for agent outputs. PlanContract covers routing plan structure for multi-agent turns.
How it works
sequenceDiagram
autonumber
participant U as User
participant P as Planner LLM
participant F as finalize_draft
participant V as validate_plan
participant D as Dispatcher
U->>P: message
P->>F: PlanDraft JSON
F->>F: merge same agents
F->>F: derive_topology()
F->>V: AgentPlan
V-->>F: ValidationReport
F->>D: execution_waves()
Note over D: wave 1: parallel agents<br/>wave 2: next in chain
Pipeline diagram (static)
flowchart LR
A[PlanDraft] --> B[merge_same_agent_tasks]
B --> C[derive_topology]
C --> D[validate_plan]
D --> E[AgentPlan]
E --> F[execution_waves]
F --> G[Dispatcher]
Install
From PyPI:
pip install plancontract
# or
uv add plancontract
Requires Python 3.11+.
From source (development):
git clone https://github.com/zamaniali1995/plancontract.git
cd plancontract
uv sync
make ci
Requires uv for development. Python 3.11+.
Quick start
from plancontract import PlanDraft, PlanPolicy, TaskDraft, finalize_draft, validate_plan
draft = PlanDraft(
tasks=[
TaskDraft(
task_id="t1",
agent_id="travel_agent",
instruction="find flights to Tokyo",
),
TaskDraft(
task_id="t2",
agent_id="billing_agent",
instruction="check travel insurance coverage",
),
],
)
plan = finalize_draft(draft, policy=PlanPolicy.demo())
print(plan.topology) # PlanTopology.PARALLEL
print(plan.execution_waves()) # one wave: [travel_agent, billing_agent]
assert validate_plan(plan, policy=PlanPolicy.demo()).ok
Run the bundled example:
uv run python examples/basic_usage.py
Example output
topology: parallel
agents: ['travel_agent', 'billing_agent']
waves:
wave 1: ['travel_agent', 'billing_agent']
valid: True
Topologies at a glance
Topology is derived from dependencies during finalize_draft() — never copied blindly from LLM output.
| Topology | Tasks | Dependency shape | Waves |
|---|---|---|---|
single |
1 | — | 1 |
parallel |
2–3 | none (independent) | 1 |
sequential |
2–3 | chain | 1 per task |
mixed |
3 | fan-in / fan-out | 2+ |
deferred |
0 | — | 0 |
fallback |
— | reserved | — |
Sequential example — draft in, plan out
Input examples/data/plan_draft.sequential.json:
{
"tasks": [
{
"task_id": "t1",
"agent_id": "travel_agent",
"instruction": "shortlist weekend destinations within 3 hours"
},
{
"task_id": "t2",
"agent_id": "billing_agent",
"instruction": "estimate total cost for the selected destination",
"depends_on": ["t1"]
}
],
"locale": "en",
"summary": "Pick a destination, then estimate cost."
}
plancontract finalize examples/data/plan_draft.sequential.json --demo-policy
{
"topology": "sequential",
"locale": "en",
"title": "",
"tasks": [
{
"task_id": "t1",
"agent_id": "travel_agent",
"instruction": "shortlist weekend destinations within 3 hours",
"depends_on": [],
"extensions": {}
},
{
"task_id": "t2",
"agent_id": "billing_agent",
"instruction": "estimate total cost for the selected destination",
"depends_on": ["t1"],
"extensions": {}
}
]
}
Validation that CI can gate on
Structured errors — not just exceptions:
from plancontract import AgentPlan, PlanPolicy, ValidationCode, validate_plan
plan = AgentPlan.model_validate_json(open("plan.json").read())
report = validate_plan(
plan,
policy=PlanPolicy(allowed_agents=frozenset({"travel_agent", "billing_agent"})),
)
if not report.ok:
for issue in report.issues:
print(issue.code, issue.message) # ValidationCode.UNKNOWN_AGENT, ...
report.raise_if_invalid() # raises PlanValidationError for CI
GitHub Actions:
- run: pip install plancontract
- run: plancontract validate plans/candidate.json --mode draft --demo-policy
Example validation failure (CLI)
plancontract validate bad-plan.json --mode draft --demo-policy
{
"ok": false,
"error_count": 1,
"issues": [
{
"code": "unknown_agent",
"message": "unknown or disallowed agent_id 'unknown_agent'",
"task_id": "t1",
"field_name": "agent_id",
"context": {}
}
]
}
Use --mode draft for LLM planner JSON; omit it (default plan) for finalized AgentPlan files.
CLI
| Command | Purpose |
|---|---|
plancontract validate <file> |
Validate a plan or draft JSON |
plancontract finalize <draft> |
Merge, derive topology, validate |
plancontract schema draft | plan |
Print JSON Schema |
# Validate LLM draft with demo agent allowlist
plancontract validate examples/data/plan_draft.parallel.json --mode draft --demo-policy
# Finalize draft → AgentPlan
plancontract finalize examples/data/plan_draft.sequential.json --demo-policy
# Export schemas (also committed under schemas/)
plancontract schema draft > plan_draft.schema.json
plancontract schema plan > agent_plan.schema.json
Core concepts
| Type | Role |
|---|---|
TaskDraft |
One LLM-proposed agent task |
PlanDraft |
Planner output (tasks + locale + summary) |
PlanTask |
Runtime task with validated fields |
AgentPlan |
Finalized plan with derived PlanTopology |
PlanPolicy |
Allowlists, limits, extension rules |
ValidationReport |
Pass/fail with machine-readable ValidationCode |
Public API:
from plancontract import (
AgentPlan,
PlanDraft,
PlanPolicy,
PlanTopology,
ValidationCode,
derive_topology,
finalize_draft,
merge_same_agent_tasks,
topological_waves,
validate_plan,
)
See docs/design.md for architecture notes and schemas/ for committed JSON Schema.
Project structure
plancontract/
├── assets/ # README visuals (SVG source + PNG for GitHub)
├── docs/
│ └── design.md # Architecture notes
├── examples/
│ ├── basic_usage.py # Runnable quick-start script
│ └── data/
│ ├── plan_draft.parallel.json
│ └── plan_draft.sequential.json
├── schemas/
│ ├── plan_draft.schema.json # Committed JSON Schema
│ └── agent_plan.schema.json
├── src/plancontract/
│ ├── __init__.py # Public API
│ ├── __main__.py # python -m plancontract
│ ├── py.typed # PEP 561 typing marker
│ ├── models.py # Pydantic schemas (PlanDraft, AgentPlan, …)
│ ├── policy.py # PlanPolicy constraints
│ ├── topology.py # Graph scheduling + topology derivation
│ ├── merge.py # Same-agent task merge
│ ├── validate.py # Structured validation
│ ├── finalize.py # Draft → plan pipeline
│ ├── errors.py # ValidationReport and codes
│ └── cli.py # plancontract CLI
└── tests/
├── conftest.py # Shared fixtures
├── helpers.py # Test task builders
├── test_topology.py # Graph + derive_topology
├── test_validate.py # Validation rules
├── test_finalize.py
├── test_merge.py
├── test_models.py
├── test_errors.py
├── test_cli.py
├── test_schemas.py
└── test_integration.py
Roadmap
- LangGraph adapter (
PlannerNode,DispatchNode) - RouteBench scorer integration
- JSON Schema publishing (
schemas/) -
plancontract difffor eval regression
Contributing & license
Contributions welcome — see CONTRIBUTING.md. Run make ci before opening a PR.
License: MIT
Disclaimer: Independent open-source project. Not affiliated with any employer or downstream product.
If PlanContract saves you from rewriting planner validation for the third time, consider starring the repo.
Built for teams shipping multi-agent assistants with explicit planner → dispatcher contracts.
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 plancontract-0.1.3.tar.gz.
File metadata
- Download URL: plancontract-0.1.3.tar.gz
- Upload date:
- Size: 22.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9001dfefaf7238ad71d6fe6231d25810fa6d9c77b241b797870e1eb7b2c7d7e2
|
|
| MD5 |
c70c5728164a955d1e31f345353d55df
|
|
| BLAKE2b-256 |
2db0457f8f04bb4bde2486aeb0ae2f0cfa758300cb9de60ab060fc061dc0dd07
|
File details
Details for the file plancontract-0.1.3-py3-none-any.whl.
File metadata
- Download URL: plancontract-0.1.3-py3-none-any.whl
- Upload date:
- Size: 20.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
402632e492a5ab63844a4f670e4438f4b3bf7a2f66d304452bfa6423582ae34f
|
|
| MD5 |
e3a22b76b1836a1c1f4b36715d50ee5a
|
|
| BLAKE2b-256 |
bb19d4b5edfa240bba144f74f55909aa3e1072e4d7014ddb13540ce8261d20db
|