Adapter guarantee matrix
Every integration below records work in Pyrula’s journal rather than only matching
another framework’s API. The three drop-ins (compat.openai,
compat.anthropic and durable_chat_model) do that inside a Pyrula run and pass
through unchanged outside one; the other three own a run of their own. See
what’s durable, and what isn’t for that
distinction and how to move work across it.
Every cell maps to a conformance test. yes means the contract asserts the
guarantee holds; no means a test asserts the work re-executes and is not journaled;
partial means it holds within the stated boundary; n/a means the capability has no
meaning for that surface.
| Integration | streaming | structured output | tools | cancellation | restart | duplicate request |
|---|---|---|---|---|---|---|
native | yes | yes | yes | yes | yes | yes |
compat.openai | partial | partial | partial | n/a | n/a | n/a |
compat.anthropic | partial | partial | partial | n/a | n/a | n/a |
as_langgraph_node | n/a | n/a | n/a | yes | yes | partial |
durable_chat_model | partial | yes | partial | n/a | n/a | n/a |
mcp.server | n/a | n/a | n/a | yes | yes | partial |
mcp.client | n/a | n/a | n/a | n/a | n/a | n/a |
Column definitions
Section titled “Column definitions”- streaming: A stream consumed live is journaled chunk by chunk as it arrives.
- A replay serves the recorded response without calling the provider: chunk for chunk on the compat drop-ins, as the recorded message on native (whose replay is silent).
- A stream abandoned partway is not journaled at all, so a truncated response is never replayed as if it were complete.
- structured output: A structured (schema-validated) response is journaled whole.
- A replay returns the identical parsed value without calling the provider.
- tools: The tool calls a model requests are journaled verbatim.
- A replay returns the same requested calls without asking the provider, so a resumed run takes the branch it took live.
- Executing a requested call is your code: durable only if it runs inside a step or a @tool. This guarantee does not provide that.
- cancellation: A cancel is requested on the store and observed cooperatively.
- The run body sees it only at its own boundaries: ctx.cancelled checks, ctx.run_child’s wait, and between native LLM calls. A step, gather or sleep already in flight is not interrupted.
- A cancel that arrives while no body is running (still queued, or resuming after a crash) is checked before the body is entered, so that attempt ends cancelled without running.
- The node or tool call awaiting the run ends with DurableInvocationCancelled once the run’s terminal reflects the cancel.
- Cancelling the awaiting caller’s own task only detaches it; the run keeps going, and a later re-invocation re-attaches.
- restart: A run whose worker dies mid-attempt resumes from its journal: completed steps and calls are read back, not re-executed, and their side effects are not repeated.
- duplicate request: A request submitted twice under one idempotency token attaches to the run the first submission created instead of starting a second.
- The token lasts its own TTL; the run stays readable for the completed-run retention. Set retention to at least the token TTL if a late resubmission must read the result. A token that outlives its run still attaches rather than re-running the work.
- Without a token, resubmitting an existing run id is refused, not attached: the token is how a caller says a resubmission is expected.
Install and tested versions
Section titled “Install and tested versions”| Integration | Extra | Tested versions |
|---|---|---|
native | built in | no third-party SDK |
compat.openai | pyrula-agents[openai] | openai 1.3.8 – 2.41.0 |
compat.anthropic | pyrula-agents[anthropic] | anthropic 0.40 – 0.106.0 |
as_langgraph_node | pyrula-agents[langgraph] | langgraph 0.2.17 – 1.2.4 |
durable_chat_model | pyrula-agents[langchain] | langchain-core 0.3.0 – 1.4.1 |
mcp.server | pyrula-agents[mcp] | mcp 1.0 – 1.27.2 |
mcp.client | pyrula-agents[mcp] | mcp 1.0 – 1.27.2 |
durable_chat_model: only the async path (ainvoke/astream) journals; the synchronous path (invoke, via _generate) raises UnjournaledSurface inside a run rather than silently forwarding to the provider live and unjournaledmcp.client: each tool a server exposes (mcp_servers=, ctx.mcp_tools) is an ordinary @tool: its result is recorded the first time it runs and replayed on recovery without calling the server again, so a run that already used a server replays even while that server is down. A call that still fails after its retries is recorded as that error and replays as it, as for any @tool
Limits
Section titled “Limits”compat.openai- streaming;compat.anthropic- streaming: via create(stream=True); the SDK’s stream() helper is refused inside a runcompat.openai- structured output: via create(response_format=…); the SDK’s parse() helper is refused inside a runcompat.openai- tools;compat.anthropic- tools;durable_chat_model- tools: the tool calls the model requested are journaled and replay verbatim; executing them is the caller’s code, durable only if it runs in a step or @toolcompat.openai- cancellation, restart, duplicate request;compat.anthropic- cancellation, restart, duplicate request;durable_chat_model- cancellation, restart, duplicate request;mcp.client- cancellation, restart, duplicate request: this surface contributes one journaled call to an enclosing run and has no run identity of its own; the run’s lifecycle is the native rowcompat.anthropic- structured output: via a tool-forced schema; the SDK’s parse() helper is refused inside a runas_langgraph_node- streaming: the node submits and awaits one durable run; it does not stream tokensas_langgraph_node- structured output: the node returns the workflow’s own result under output_key, not a model responseas_langgraph_node- tools: the wrapped workflow’s tools are native @tools, covered by the native rowas_langgraph_node- duplicate request: run identity is thread_id::node_key, so a LangGraph resume re-attaches the same run; a cyclic graph revisiting the node re-attaches too, and a fresh run per iteration needs a per-iteration node_keydurable_chat_model- streaming: the wrapper never overrides _stream/_astream, so astream() journals the whole response as one atomic call rather than recording chunks as they arrive; replay serves that response from the journal without calling the provider again, but a caller who abandons the stream after one chunk has already paid for a fully journaled callmcp.server- streaming: a tool call returns one durable result; the server does not stream partialsmcp.server- structured output: the tool’s return value is its own shape, not a model responsemcp.server- tools: this row is the outbound durable tool server, not a model client: there is no model asking it for tool calls to replay. Each tool it exposes becomes a durable run instead, and that run’s durability is what this row’s restart and duplicate request cells prove. The inbound MCP client is themcp.clientrowmcp.server- duplicate request: strong when the client sends _meta.idempotency_key, which survives a reconnect; without one each call is its own run, so a retry re-runs the tool. The JSON-RPC request id is not a fallback: ids restart per session, and two clients sharing one would read each other’s resultmcp.client- streaming: a remote tool call returns one result; nothing is streamedmcp.client- structured output: the tool’s result is the server’s own shape, not a model responsemcp.client- tools: this row is a tool source, not a model client: the model that asks for these tools is the native row, whose tools cell governs the request. What this row adds is on the execution side - see the row note