Skip to main content

PlanContract — planner output in, validated plan out


Framework-agnostic schemas and validators for multi-agent turn routing plans.

Routing plan contracts — not behavioral agent safety contracts.


Python 3.11+ PyPI License: MIT Typed CI Ruff Pydantic v2


Install · Quick start · Topologies · CLI · Design · Contributing


Table of contents


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.

Parallel vs sequential topology

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 diff for 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.

Metadata

Release files for plancontract 0.1.3

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

Source distribution (sdist)

Source distribution for plancontract 0.1.3
File Size Uploaded
plancontract-0.1.3.tar.gz 22.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for plancontract 0.1.3
File Interpreter ABI Platform
plancontract-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 43.4 kB

Release files / plancontract-0.1.3.tar.gz

Download URL plancontract-0.1.3.tar.gz
Size 22.5 kB
Tags Source
SHA-256 checksum
How to use checksums
9001dfefaf7238ad71d6fe6231d25810fa6d9c77b241b797870e1eb7b2c7d7e2
BLAKE2b-256 checksum
How to use checksums
2db0457f8f04bb4bde2486aeb0ae2f0cfa758300cb9de60ab060fc061dc0dd07
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / plancontract-0.1.3-py3-none-any.whl

Download URL plancontract-0.1.3-py3-none-any.whl
Size 20.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
402632e492a5ab63844a4f670e4438f4b3bf7a2f66d304452bfa6423582ae34f
BLAKE2b-256 checksum
How to use checksums
bb19d4b5edfa240bba144f74f55909aa3e1072e4d7014ddb13540ce8261d20db
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

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