pi-durable-python
A Python port of @earendil-works/pi-durable
v1.3.0: durable conversations, tasks, and documents for agents, on asyncio.
The process can die at any point. A run, a tool round, a compaction, or an inbox placement only happens after the state it depends on is committed, so a restart resumes from what storage holds rather than from what memory held.
- Durable by construction. Every effect is sandwiched between committed state: tasks re-enter their checkpoint, submissions survive a crash, documents version as they change.
- Small core. The base install has no runtime dependencies, and the memory, SQLite, and JSONL storage backends all ship in it. Model adapters and JSON Schema validation are separate extras.
- Composable. A
Sessionis the kernel; theHarnessadds conversations, submissions, scheduling, compaction, extensions, and hooks on top of it. - Parity, not resemblance. This is a mechanical translation of the v1.3.0 source held to its
semantics and persisted record shapes.
PARITY.mdis the contract.
Requirements
- Python. CPython 3.11 or newer. The verified set is 3.11, 3.12, 3.13, 3.14, and 3.15 on Linux and macOS, plus the 3.14t and 3.15t free-threaded builds; the classifiers list the plain CPython versions 3.11 through 3.15.
- Free-threaded parity. The suite passes on builds with the GIL disabled, so the code does not
depend on it. Nothing claims thread safety: do not share one
SessionorStoragebetween threads. Concurrentasynciotasks are the supported concurrency. - Dependencies. The core package has no runtime dependencies. The
openaiandanthropicextras carry their SDKs, andschemacarriesjsonschema.
Install
pip install pi-durable-python
Extras are independent; install only the ones you use:
| Extra | Adds | Install |
|---|---|---|
openai |
OpenAI Chat Completions adapter (AsyncOpenAI) |
pip install "pi-durable-python[openai]" |
anthropic |
Anthropic Messages adapter (AsyncAnthropic) |
pip install "pi-durable-python[anthropic]" |
schema |
JSON Schema validation for structured output and tool arguments | pip install "pi-durable-python[schema]" |
all |
Everything in openai, anthropic, and schema |
pip install "pi-durable-python[all]" |
dev |
pytest, pytest-asyncio, ruff, and mypy | pip install "pi-durable-python[dev]" |
From a checkout, uv sync --extra dev or
python -m pip install -e ".[dev,openai,anthropic,schema]" matches CI.
Quick start
A durable session and a document
A Session is the only writer: every change goes through commit, and readers see committed
state. Documents are typed JSON records, defined once and used as tokens.
import asyncio
from pi_durable import MemoryStorage, create_session, define_doc
from pi_durable.context import BACKGROUND_CONTEXT
# Define once: the kind, version, scope, and initial value migrate and materialize the document.
CounterDoc = define_doc(
{"kind": "demo.counter", "version": 1, "scope": "session", "initial": lambda: {"count": 0}}
)
async def main() -> None:
session = create_session(MemoryStorage())
async def bump(tx):
counter = await tx.doc(CounterDoc) # a draft, materialized only if the commit lands
counter.count += 1
await session.commit(bump, BACKGROUND_CONTEXT)
async def bump_again(tx):
(await tx.doc(CounterDoc)).count += 1
await session.commit(bump_again, BACKGROUND_CONTEXT)
snapshot = await session.snapshot(CounterDoc, BACKGROUND_CONTEXT)
print(snapshot) # {'count': 2}
await session.close(BACKGROUND_CONTEXT)
asyncio.run(main())
Snapshots, document states, and watches are disposable reads: snapshot() copies the committed
value, document_state() subscribes to later commits, and watch_doc() delivers every committed
frame. MemoryStorage is the reference backend; SQLite and JSONL live in
pi_durable.storage.sqlite and pi_durable.storage.jsonl.
A conversation
The Harness adds conversations on top of a Session. A submission is the only entry point: it is
admitted to the inbox, placed as an entry, and settled once a run answers it. Passive writes (such
as seeding a transcript) need no model at all.
import asyncio
from pi_durable import Harness, MemoryStorage, UserEntry, create_registry
from pi_durable.context import BACKGROUND_CONTEXT
async def main() -> None:
harness = await Harness.open(
MemoryStorage(), {"models": {}, "registry": create_registry()}, BACKGROUND_CONTEXT
)
try:
conversation = await harness.root(BACKGROUND_CONTEXT)
await conversation.submit(
{
"type": "write",
"entry": {
"kind": UserEntry.kind,
"model": [{"role": "user", "content": "hello", "timestamp": 0}],
},
},
BACKGROUND_CONTEXT,
)
view = await conversation.context(BACKGROUND_CONTEXT)
print([message["role"] for message in view["messages"]]) # ['user']
state = await conversation.view_state(BACKGROUND_CONTEXT)
print([entry["kind"] for entry in state.value["entries"]]) # ['pi.user']
state.dispose()
finally:
await harness.close(BACKGROUND_CONTEXT)
asyncio.run(main())
Answering with a model
The provider adapters implement the Models protocol, so the core never imports an SDK. A model
reference in an agent change uses the persisted camelCase spelling. Opening a Harness starts it
paused, and admitting a submission resumes scheduling, so a host can restore state before work
begins.
import asyncio
from pi_durable import Harness, MemoryStorage, Model, create_registry
from pi_durable.context import BACKGROUND_CONTEXT
from pi_durable.providers.openai import OpenAIModels
async def main() -> None:
model = Model(provider="openai", model_id="gpt-4.1", context_window=1_000_000, max_tokens=32_768)
models = OpenAIModels([model]) # reads OPENAI_API_KEY, or pass api_key=...
harness = await Harness.open(
MemoryStorage(), {"models": models, "registry": create_registry()}, BACKGROUND_CONTEXT
)
try:
conversation = await harness.root(
BACKGROUND_CONTEXT,
{"agent": {"model": {"provider": "openai", "modelId": "gpt-4.1"}}},
)
submission = await conversation.submit(
{"type": "input", "content": "Explain durable execution in one sentence."},
BACKGROUND_CONTEXT,
)
settled = await submission.wait(BACKGROUND_CONTEXT)
print(settled["status"]) # 'done' or 'unanswered'
finally:
await harness.close(BACKGROUND_CONTEXT)
asyncio.run(main())
The answer is an entry, not a return value: settled["answer"] names the assistant entry, which the
conversation view and the entry scan both expose. This snippet needs credentials, so the test suite
does not run it; README examples that run offline carry an <!-- example --> marker and are
executed by tests/test_release_docs.py.
API at a glance
| Module | Holds |
|---|---|
pi_durable |
Root export: Session/create_session, Harness, entry tokens, document and task definitions, errors, provider protocol, MemoryStorage, and the built-in task types. |
pi_durable.env |
The ExecutionEnv contract, the POSIX backend, output truncation, and line scanning. |
pi_durable.harness |
Conversations, submissions and the inbox, the scheduler, agents and the registry, extensions and hooks, tools, generation, compaction, usage, the conversation view, the agent event stream, and the task graph. |
pi_durable.providers |
The Model catalog entry, the Models protocol, transcript and request helpers, retry and overflow classifiers, and the OpenAI/Anthropic adapters (pi_durable.providers.openai, pi_durable.providers.anthropic). |
pi_durable.session |
The Session kernel, the transaction API, document drafts, forks, and observation. |
pi_durable.storage |
The reference memory backend (pi_durable.storage.memory), SQLite (pi_durable.storage.sqlite), and JSONL (pi_durable.storage.jsonl). |
pi_durable.testing |
Storage and environment conformance suites, plus assertions for writing your own backend. |
pi_durable.tool_search |
BM25 deferred-tool discovery for large tool sets. |
pi_durable.tools |
The built-in coding tools: read, write, edit, and bash. |
The usage guide walks through documents, tasks, the Harness, providers, storage
backends, and extensions with runnable examples. The normative details are in the
vendored 1.3.0 specification; the parity matrix is
PARITY.md, and the audit report lists the translated
suites and the remaining explicit deviations.
Stability and versioning
- Lockstep. The package version tracks the upstream release it translates: 1.3.0 here is
@earendil-works/pi-durablev1.3.0 (tagv1.3.0, commit71023aec1546d6d677a65669c5665c0927ce0dd0). Adopting a later upstream release is a version bump, not a routine sync. - Parity is semantics and persisted shapes. Python names follow PEP 8 while classes stay PascalCase and persisted JSON keeps camelCase, so a stored record round-trips as the specification describes. Cross-language storage interoperability is incidental, not a contract; see ADR 0001.
- Stable surface. The modules in the table above, the names exported from them, and the
specification sections they implement.
AgentEventStreamis experimental, as upstream says. - Excluded. Code Mode, the image implementation (the
ImageProcessorprotocol remains), Cloudflare adapters, PowerShell, Windows support, and the benchmark helpers.PARITY.mdmarks every exclusion.
Development
uv sync --extra dev,openai,anthropic,schema
uv run ruff check src tests
uv run mypy
uv run pytest
CI runs the suite on Linux and macOS for Python 3.11–3.15 and the two free-threaded builds, builds
and installs the sdist and wheel in fresh environments, then installs each extra from the wheel to
prove the extras resolve and stay independent. docs/release.md records the
full release steps: verify, build, tag, publish.
License
MIT. The vendored v1.3.0 specification is MIT, copyright (c) 2025 Mario Zechner; see
docs/upstream/LICENSE.
Metadata
Release files for pi-durable-python 1.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pi_durable_python-1.3.0.tar.gz | 703.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pi_durable_python-1.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.0 MB
Release files / pi_durable_python-1.3.0.tar.gz
| Download URL | pi_durable_python-1.3.0.tar.gz |
|---|---|
| Size | 703.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
48c388ccb1e78daec9b11b7b8ccc774cff47d55141d4594142aa3d59351ab16f
|
|
BLAKE2b-256 checksum How to use checksums |
881f9f16295067dba6b7d5fb72f237d228599c2d6d6288be3b455671e97f963c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.15.0
|
Release files / pi_durable_python-1.3.0-py3-none-any.whl
| Download URL | pi_durable_python-1.3.0-py3-none-any.whl |
|---|---|
| Size | 301.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2232dc4f5287492944efd451edb7bf2030ef98d182545d417eebc841745de71b
|
|
BLAKE2b-256 checksum How to use checksums |
805213ae5a07567b60ce2791a53e9fe86ede1529f2cbdbeb56b83f29e659f244
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.15.0
|
Security for pi-durable-python 1.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.