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.

Integrations

Everything imports from one place:

from pyrula.agents.integrations import (
from_langchain_tool,
durable_chat_model,
as_langgraph_node,
)

PyrulaDurability needs pip install "pyrula[agents,pydantic-ai]" and is imported on its own:

from pyrula.agents.integrations import PyrulaDurability
  • Tool adapters: from_langchain_tool wraps a LangChain tool as a Pyrula @tool, and durable_chat_model journals a LangChain chat model. See LangChain tools.
  • Durable nodes: as_langgraph_node makes one node of an existing graph a journaled, resumable Pyrula run. See LangGraph.
  • Tool servers: build_mcp_server serves Pyrula tools to Claude Code, Cursor or any MCP host. See Serving tools over MCP; Calling MCP servers is the other direction.
  • Capabilities: PyrulaDurability journals a Pydantic AI agent’s model and tool calls. See Pydantic AI.
  • Recipes: the OpenAI Agents SDK runs on the journaled AsyncOpenAI with no adapter. See OpenAI Agents SDK.

Which side owns the journal decides what is durable: What’s durable, and what isn’t.

IntegrationInstall
LangChain toolspip install "pyrula[agents,langchain]"
LangGraphpip install "pyrula[agents,langgraph]"
Pydantic AIpip install "pyrula[agents,pydantic-ai]"
OpenAI Agents SDKpip install "pyrula[agents]" openai-agents (in an environment without pyrula[litellm])
MCP tool server and MCP clientpip install "pyrula[agents]"

run_and_await submits a workflow idempotently and waits for its result, for frameworks without an adapter:

from pyrula.workflows import run_and_await
result = await run_and_await(
store, "charge_order", {"order": "o-1"},
idempotency_key="charge:o-1", # becomes the run id; a retry re-attaches
timeout=60,
)

It raises DurableInvocationError if the run fails, DurableInvocationTimeout past timeout, and DurableInvocationCancelled if cancelled. A worker serving the workflow must be running.

Pass on_interrupt= to answer a ctx.interrupt from the caller. Without it the interrupt surfaces as DurableInterrupt. The handler’s return value is the answer, or InterruptResponse(action, value) with action one of approve, reject, respond or edit (anything else raises ValueError). A handler that raises or returns a bad action ends the run with interrupt_handler_failed; raise DurableInterrupt to leave the run suspended. On an approval interrupt (expects="approval") the bare string "approve" or "reject" is shorthand, and any other return value is a respond answer. To respond with the literal text “approve”, return InterruptResponse("respond", "approve").

View this page as Markdown