# Integrations

> Bring LangChain tools into a Pyrula turn, run a durable Pyrula workflow as a LangGraph node, make a Pydantic AI agent or an OpenAI Agents SDK run durable, and serve Pyrula tools to any MCP host.

Source: https://docs.pyrula.io/agents/integrations/

Everything imports from one place:

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

```python
from pyrula.agents.integrations import PyrulaDurability
```

## Patterns

- Tool adapters: `from_langchain_tool` wraps a LangChain tool as a Pyrula [`@tool`](https://docs.pyrula.io/agents/authoring/tools.md), and `durable_chat_model` journals a LangChain chat model. See [LangChain tools](https://docs.pyrula.io/agents/integrations/langchain.md).
- Durable nodes: `as_langgraph_node` makes one node of an existing graph a journaled, resumable Pyrula run. See [LangGraph](https://docs.pyrula.io/agents/integrations/langgraph.md).
- Tool servers: `build_mcp_server` serves Pyrula tools to Claude Code, Cursor or any MCP host. See [Serving tools over MCP](https://docs.pyrula.io/agents/integrations/mcp-server.md); [Calling MCP servers](https://docs.pyrula.io/agents/integrations/mcp.md) is the other direction.
- Capabilities: `PyrulaDurability` journals a Pydantic AI agent's model and tool calls. See [Pydantic AI](https://docs.pyrula.io/agents/integrations/pydantic-ai.md).
- Recipes: the OpenAI Agents SDK runs on the journaled `AsyncOpenAI` with no adapter. See [OpenAI Agents SDK](https://docs.pyrula.io/agents/integrations/openai-agents-sdk.md).

Which side owns the journal decides what is durable: [What's durable, and what isn't](https://docs.pyrula.io/agents/integrations/durable-vs-not.md).

| Integration | Install |
|---|---|
| LangChain tools | `pip install "pyrula[agents,langchain]"` |
| LangGraph | `pip install "pyrula[agents,langgraph]"` |
| [Pydantic AI](https://docs.pyrula.io/agents/integrations/pydantic-ai.md) | `pip install "pyrula[agents,pydantic-ai]"` |
| [OpenAI Agents SDK](https://docs.pyrula.io/agents/integrations/openai-agents-sdk.md) | `pip install "pyrula[agents]" openai-agents` (in an environment without `pyrula[litellm]`) |
| [MCP tool server](https://docs.pyrula.io/agents/integrations/mcp-server.md) and [MCP client](https://docs.pyrula.io/agents/integrations/mcp.md) | `pip install "pyrula[agents]"` |

## Custom integrations

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

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