Skip to content
Pyrula
Pre-release: the packages are not on PyPI yet, so pip install does not resolve. Build from source to try it today.

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.

Integrationstreamingstructured outputtoolscancellationrestartduplicate request
nativeyesyesyesyesyesyes
compat.openaipartialpartialpartialn/an/an/a
compat.anthropicpartialpartialpartialn/an/an/a
as_langgraph_noden/an/an/ayesyespartial
durable_chat_modelpartialyespartialn/an/an/a
mcp.servern/an/an/ayesyespartial
mcp.clientn/an/an/an/an/an/a
  • 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.
IntegrationExtraTested versions
nativebuilt inno third-party SDK
compat.openaipyrula-agents[openai]openai 1.3.8 – 2.41.0
compat.anthropicpyrula-agents[anthropic]anthropic 0.40 – 0.106.0
as_langgraph_nodepyrula-agents[langgraph]langgraph 0.2.17 – 1.2.4
durable_chat_modelpyrula-agents[langchain]langchain-core 0.3.0 – 1.4.1
mcp.serverpyrula-agents[mcp]mcp 1.0 – 1.27.2
mcp.clientpyrula-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 unjournaled
  • mcp.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
  • compat.openai - streaming; compat.anthropic - streaming: via create(stream=True); the SDK’s stream() helper is refused inside a run
  • compat.openai - structured output: via create(response_format=…); the SDK’s parse() helper is refused inside a run
  • compat.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 @tool
  • compat.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 row
  • compat.anthropic - structured output: via a tool-forced schema; the SDK’s parse() helper is refused inside a run
  • as_langgraph_node - streaming: the node submits and awaits one durable run; it does not stream tokens
  • as_langgraph_node - structured output: the node returns the workflow’s own result under output_key, not a model response
  • as_langgraph_node - tools: the wrapped workflow’s tools are native @tools, covered by the native row
  • as_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_key
  • durable_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 call
  • mcp.server - streaming: a tool call returns one durable result; the server does not stream partials
  • mcp.server - structured output: the tool’s return value is its own shape, not a model response
  • mcp.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 the mcp.client row
  • mcp.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 result
  • mcp.client - streaming: a remote tool call returns one result; nothing is streamed
  • mcp.client - structured output: the tool’s result is the server’s own shape, not a model response
  • mcp.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