Metadata-Version: 2.4
Name: pollard
Version: 0.4.0
Summary: Governed execution trees for AI agents: budget it, gate it, replay it.
Project-URL: Homepage, https://github.com/jemsbhai/pollard
Project-URL: Issues, https://github.com/jemsbhai/pollard/issues
Author-email: Muntaser Syed <jemsbhai@gmail.com>
License: MIT
License-File: LICENSE
Keywords: agents,audit,budget,execution-tree,llm,mcp,replay
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: hashrope>=0.2.2; extra == 'dev'
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: tokenmaster>=0.1.0; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Provides-Extra: hashrope
Requires-Dist: hashrope>=0.2.2; extra == 'hashrope'
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == 'mcp'
Provides-Extra: nvml
Requires-Dist: nvidia-ml-py>=12.535; extra == 'nvml'
Provides-Extra: schema
Requires-Dist: jsonschema>=4.21; extra == 'schema'
Provides-Extra: tokenmaster
Requires-Dist: tokenmaster>=0.1.0; extra == 'tokenmaster'
Description-Content-Type: text/markdown

# pollard

Governed execution trees for AI agents: budget it, gate it, replay it.

```powershell
pip install pollard
```

```python
from pollard import Budget, Runtime
from examples.mock_model import call_model

rt = Runtime("runs.db")
with rt.run("triage", budget=Budget(tokens=120_000, depth=8)) as run:
    node = run.model_call(
        {"model": "mock-1", "messages": [{"role": "user", "content": "Summarize: ..."}]},
        fn=call_model,
    )
    print(node.result["text"])
    print(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.

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.4:

- 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.
- The audit tree is tamper-evident, not tamper-proof. Verification detects changed history, but it cannot stop deletion of the whole store file.

## 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 raises `MissingRecording`.

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:

```python
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:

```python
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:

```powershell
pip install "pollard[hashrope]"
```

```python
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:

```powershell
pip install "pollard[tokenmaster]"
```

```python
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.
