Governed execution trees for AI agents: budget it, gate it, replay it.
Project description
pollard
Governed execution trees for AI agents: budget it, gate it, replay it.
pip install "pollard[openai]"
from openai import OpenAI
from pollard import Budget, Runtime
from pollard.adapters.openai import make_responses_fn
with Runtime("runs.db").run("triage", budget=Budget(tokens=20_000)) as run:
node = run.model_call({"model": "gpt-5.5", "input": "Summarize: ..."},
fn=make_responses_fn(OpenAI()))
print(node.result["text"], run.report())
pollard is a runtime primitive, not an agent framework. It records each step as a node in a content-addressed tree. Node identity is a hash of the step inputs, parent identity, kind, and attempt number, so the tree gives you a control-flow ledger without owning your model client, tools, prompts, or loop.
The client above belongs to your code. Pollard does not read credentials or
construct provider clients. Anthropic and LiteLLM adapters follow the same
pattern through pollard[anthropic] and pollard[litellm].
What you get:
- Budget: refuse a step before it runs when a known budget would be exceeded.
- Branch and rollback: make alternate children, move the cursor back, and keep shared history.
- Audit: each node id commits to its ancestry and identity payload.
- Registry firewall: registered tool calls resolve against a versioned action set or fail closed.
- Replay: record semantic steps once, then serve stored results in tests and CI.
Budget semantics are honest about what can be controlled. If a precheck estimate proves a step would exceed budget, pollard records a refusal node and does not call your function. If the actual result charge exceeds budget after the function returns, that node still stands because the spend already happened; later steps are refused.
Limits in v0.5:
- Replay of sampled model calls serves the recorded output. It does not re-check that a provider would return that output again.
- Hosted API energy use is not measured. The NVML energy meter is for local GPU inference only.
- A SQLite store assumes one writer process.
- HashRopeStore is an in-process append-only snapshot backend, not a multi-writer database.
- TokenmasterMeter reports tokenmaster state from the usage data your model client returns; it does not tokenize prompts itself.
- Prompt estimators are approximations. Images, tool schemas, provider-added instructions, and wire-format changes can make the settled usage differ.
- The audit tree is tamper-evident, not tamper-proof. Verification detects changed history, but it cannot stop deletion of the whole store file.
Offline Mock Demo
Core Pollard still installs with zero runtime dependencies and can be tried without a provider account:
from pollard import Budget, Runtime
from examples.mock_model import call_model
with Runtime().run("offline", budget=Budget(tokens=100)) as run:
node = run.model_call({"model": "mock-1", "messages": []}, fn=call_model)
print(node.result["text"])
Streaming And Estimates
A model function may return a result dictionary or an iterator of chunk
dictionaries. model_call(..., on_delta=callback) forwards chunks in order.
With keep_chunks=True, Pollard stores those chunks under result["chunks"]
and re-emits them through the callback during replay. Charges settle once, after
the stream ends, and node identity remains a function of the input payload.
TokenMeter(estimator=..., reserved_output_tokens=N) applies an estimated input
charge plus an explicit output reservation at precheck. A refusal caused by that
estimate records {"estimated": "true"}. The settled provider usage remains the
source of actual token charges.
The optional tiktoken estimator is available as:
from pollard.estimators.openai import OpenAITokenEstimator
from pollard.meters import TokenMeter
meter = TokenMeter(OpenAITokenEstimator(), reserved_output_tokens=1024)
See docs/recipes/ for full tool loops and integration patterns.
Branch, Rollback, And Shared Prefixes
run.branch() creates an alternate child cursor while leaving the parent cursor
unchanged. run.rollback() moves a cursor to an ancestor, and run.prune()
marks an unwanted tip without deleting history. Identical calls beneath the same
parent compute the same node id, so hybrid and replay modes reuse recorded
prefixes before branches diverge.
EXP-001 measured this behavior only with deterministic mock token accounting.
Its local-model, wall-clock, dollar, and joule legs remain unrun. See
LOGBOOK.md and findings.md for the exact scope and results.
Registry Firewall
With a registry installed, tool_call cannot execute an arbitrary caller-supplied function. The runtime resolves the tool name and version against ActionSpec, validates arguments against the supported schema subset, records the spec_digest and registry_digest, then runs the registered handler. Unknown tools, version mismatch, invalid args, policy denial, and missing confirmation all produce refusal nodes.
This is structural gating, not content judgment. A content firewall tries to decide whether a requested action is safe. pollard answers a narrower audit question: was this action in the declared, versioned set, with arguments that match its schema, under the recorded policy state?
Dry-run mode records side-effectful registered actions without executing their handlers. This is useful for reviewing an intended action transcript before allowing writes.
How it compares:
- LangGraph and related graph runtimes execute a graph you author ahead of time. pollard ledgers the control flow your code performs and can wrap calls inside a graph node.
- pydantic-ai, smolagents, and the OpenAI Agents SDK own more of the agent loop. pollard is bring-your-own-client and has zero core runtime dependencies.
- Action firewall products judge tool calls by content policy. pollard uses structural registry gating: an action resolves against a versioned registry or it does not execute.
- HTTP recorders pin transport bytes. pollard pins semantic steps, so recordings can outlive SDK or provider changes.
Record And Replay
Runtime(mode=...) accepts three modes:
record: execute the function and store the result.hybrid: serve a stored result when the computed node id already exists, otherwise execute and store.replay: never call the function. A missing result raisesMissingRecording.
Replay mode verifies the stored node ancestry before serving a result. When hybrid or replay serves a stored result, run.report()["avoided"] records the charges that were skipped for that run.
For pytest, install pollard with the dev extra or with pytest available, then use the fixture:
def test_agent(pollard_run):
node = pollard_run.model_call(payload, fn=real_client)
assert "invoice" in node.result["text"].lower()
Run with --pollard-mode=record, --pollard-mode=hybrid, or --pollard-mode=replay. The fixture stores small SQLite recordings under tests/pollard_recordings/ by default.
Export Seals
seal(store, root_id) returns a rolling SHA-256 report over a subtree's node ids
and result digests. The final digest can be stored beside an exported run:
from pollard import Runtime, seal
rt = Runtime()
with rt.run("audit") as run:
run.note({"status": "ready"})
report = seal(run.store, run.root_id)
print(report.digest)
print(report.to_dict())
The seal validates each visited node before hashing it. Mutable metadata is not
included; see docs/seal.md for the field-level design.
Store Backends
Core pollard includes MemoryStore and SQLiteStore. The optional hashrope backend keeps an append-only operation log inside a hashrope rope:
pip install "pollard[hashrope]"
from pollard import HashRopeStore, Runtime
store = HashRopeStore()
with Runtime(store).run("hashrope-demo") as run:
run.note({"checkpoint": "stored in a hashrope log"})
snapshot = store.to_bytes()
reopened = HashRopeStore(snapshot)
assert reopened.get(run.root_id).payload == {"run": "hashrope-demo"}
See examples/ for offline scripts that run without network access.
Tokenmaster Meter
The optional tokenmaster meter records Pollard model-call usage into tokenmaster and stores the resulting gauge plus advice on each node:
pip install "pollard[tokenmaster]"
from pollard import Budget, Runtime
from pollard.meters import StepMeter, TokenmasterMeter
rt = Runtime(
meters=[
StepMeter(),
TokenmasterMeter(model="anthropic:claude-sonnet-4-6", expected_remaining_turns=5),
]
)
with rt.run("tokenmaster-demo", budget=Budget(tokens=120_000, steps=20)) as run:
node = run.model_call(
{"model": "anthropic:claude-sonnet-4-6"},
fn=lambda _payload: {"usage": {"input_tokens": 1000, "output_tokens": 300}},
)
print(node.meta["charges"]["tokens"])
print(node.meta["tokenmaster"]["state"]["zone"])
Use TokenmasterMeter instead of the built-in TokenMeter when you want tokenmaster state and recommendations in the audit record. The budget charge remains the per-call token volume, including cache and reasoning token fields when present.
Evidence
Phase 4 adds LOGBOOK.md and findings.md for experiment notes. README
performance numbers are intentionally absent until a logged run supports the
same scope.
Project details
Release history Release notifications | RSS feed
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 pollard-0.5.0.tar.gz.
File metadata
- Download URL: pollard-0.5.0.tar.gz
- Upload date:
- Size: 65.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
254c7f9c7409172cf2bbc2942122446a09cb433c9ceb91d4d54aa19794bc4613
|
|
| MD5 |
76c65962bc3bb02441a80ad962bf831a
|
|
| BLAKE2b-256 |
14332ca2d6add57f5ffc1aaf5a674268a9f93f6ea5de834a7f5e62d7a9f096c9
|
File details
Details for the file pollard-0.5.0-py3-none-any.whl.
File metadata
- Download URL: pollard-0.5.0-py3-none-any.whl
- Upload date:
- Size: 47.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e93c34acfe5788ef1ff8ed0713742e6b122d5100e9765068cfbd28a83bd9c2b5
|
|
| MD5 |
fd413f83e36caa780cbb61627ffdae76
|
|
| BLAKE2b-256 |
d0d3dbc4045545f2b0f7831d0cc47a0e8cf8967ef26ef03fb6778ab512249a64
|