> ## Documentation Index
> Fetch the complete documentation index at: https://docs.plurai.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Python agent frameworks

> How the generated harness drives LangGraph, CrewAI, OpenAI Agents SDK and custom LangChain agents without changing their code.

The scan writes a harness, `simulate.py`, that runs your agent as it is. It never edits your agent's source,
never asks you to refactor or add dependency injection, and never adds decorators to your tools. Everything
the simulation needs happens inside `simulate.py`.

Supported Python frameworks:

* **LangGraph** (primary)
* **Claude Agent SDK** (the Claude Code SDK)
* **Google ADK**
* **CrewAI**
* **OpenAI Agents SDK**
* **LlamaIndex**
* **Strands Agents**
* **Custom LangChain chains** (`Chain`, `AgentExecutor`, `LLMSingleActionAgent`)
* **Custom agents with no framework**, such as a plain OpenAI or LiteLLM tool loop

A plain-Python agent with no framework works the same way. For an agent written in TypeScript or JavaScript,
see [Node agents](https://docs.plurai.ai/integrations/node-agents.md).

## What every harness does

Whatever the framework, the harness follows the same order:

**1. Stub production dependencies**

Modules that would connect to production on import (a database session, Redis, a payments client) are
replaced with `MagicMock` stubs, so importing your agent does not crash or reach production.

**2. Route tools to the mock environment**

Each tool runs in one of three modes, set per tool in `plurai.config.yaml`:

| Mode | What happens |
| - | - |
| `mock` | The call is served by the mock environment from the scenario's seeded world. Your tool's body never runs. |
| `observe` | Your real tool runs, and its result is recorded into the world so later mocked tools can read it. |
| `real` | Your real tool runs untouched. |

**3. Import your agent unchanged**

The agent is imported after its tools are patched, so it picks up the mocked versions through its normal
imports and runs its own entry point.

**4. Expose an entry point to the runner**

`simulate.py` exports `invoke_agent(payload, config)`, which the SDK calls once per turn. See
[Write a harness](https://docs.plurai.ai/sdk/write-a-harness.md) for the full contract.

The scan never converts your agent to a different framework or architecture. If mocking your agent is not
possible without a rewrite, the plugin tells you and asks how to proceed.

## LangGraph

LangGraph is the primary target. For a LangGraph agent the harness:

* **Patches tools before the graph is built.** `plurai.apply_mocks` rebinds each tool function in its module,
  so `ToolNode([get_user, process_refund])` is constructed with the mocked functions. `bind_tools` still works,
  because the wrapper keeps each tool's `name`, `description` and `args_schema`.
* **Can swap `ToolNode` for `plurai.MockToolNode`.** `MockToolNode` is a drop-in replacement that dispatches each
  tool call by its mode and returns `ToolMessage`s that match `ToolNode`'s (`name`, `tool_call_id`, `status`,
  `artifact`, content).
* **Replaces context-loading nodes.** A node that loads the user's profile or permissions from production is
  replaced with one that reads the scenario's context from `plurai.get_simulation_context()`.
* **Keeps conversation history on a checkpointer.** The SDK sends only the new user message each turn, so the
  graph must keep history keyed on `config["configurable"]["thread_id"]`. The harness compiles the graph with
  `MemorySaver` (a production checkpointer such as `PostgresSaver` is stubbed out).
* **Resumes interrupts on your agent's own choice token.** A turn that answers an `interrupt()` carries the
  user's choice in `payload["interrupt_choice"]`, and the harness resumes the graph with
  `Command(resume=choice)`.

```python simulate.py (excerpt) theme={null}
import plurai
from langgraph.checkpoint.memory import MemorySaver

plurai_tool_node = plurai.MockToolNode(
    tools=[get_user_profile, process_refund, rag_search],
    mock_config={
        "get_user_profile": "mock",
        "process_refund": "mock",
        "rag_search": "observe",
    },
)

graph.add_node("tools", plurai_tool_node)
app = graph.compile(checkpointer=MemorySaver())


def invoke_agent(payload, config=None):
    return app.invoke(payload, config={**(config or {})})
```

A tool whose real body writes graph state besides its message (a `Command(update=...)`) declares those keys
under `state_updates:` in `plurai.config.yaml`, so the mocked tool writes them too.

## CrewAI

CrewAI tools are assigned to agents, not to a graph node. The harness patches the module where your tools are
defined, then builds your crew as usual:

```python theme={null}
import plurai
import our_tools

plurai.apply_mocks(our_tools, {
    "get_user_profile": "mock",
    "process_refund": "mock",
    "search_knowledge_base": "real",
})

from crewai import Agent
agent = Agent(role="...", tools=[our_tools.get_user_profile, our_tools.process_refund])
```

Tools passed to the `Agent` constructor are already wrapped, because `apply_mocks` replaced them in the module.
A CrewAI Flow tool that writes `self.state.<attr>` declares those keys under `state_updates:`.

## OpenAI Agents SDK

The same pattern: patch the tool functions before they are handed to the agent.

```python theme={null}
import plurai
import our_tools

plurai.apply_mocks(our_tools, {"get_user_profile": "mock", "process_refund": "mock"})

from agents import Agent
agent = Agent(name="billing", tools=[our_tools.get_user_profile, our_tools.process_refund])
```

* **Run-context writes.** A function tool that stores values on its `RunContextWrapper`
  (`ctx.context.<attr> = ...`) declares them under `state_updates:`; the mock writes them onto the context, not
  into the returned message.
* **Approval gates.** `@function_tool(needs_approval=True)` is read as a tool that requires the user's
  confirmation, so judges do not grade that one prompt as a re-ask.
* **Model calls in traces.** The OpenAI Agents SDK's native turn is not a LangChain message list, so the harness
  builds the turn's chain (each assistant message with its tool calls, tool results, and the model and token
  usage the framework reports) for your trace platform to show.

## Custom LangChain chains

An older LangChain agent (`Chain`, `AgentExecutor`, `LLMSingleActionAgent`) runs exactly as you wrote it: same
chain, same output parser, same executor. The harness patches the tool functions with `apply_mocks` before the
agent is imported, so the executor calls the mocked functions when the model picks a tool.

## Agents that call tools directly

When your agent calls tool functions itself instead of through a tool node, the harness wraps each one:

* `plurai.apply_mocks(module, mock_config)` rebinds every named function on a module and returns the names it
  patched. A tool built inside a factory closure, or exported under another name, cannot be reached this way.
* `plurai.mock_wrap(func, mode="mock")` wraps a single callable.

If your agent runs its own tool loop and returns prose with no tool calls in its messages, the harness also
returns a `tool_trail` so each dispatch is counted. See [Write a harness](https://docs.plurai.ai/sdk/write-a-harness.md#return-value).

## Agent platforms

Agents built on a platform rather than in code are supported too:

* Salesforce Agentforce
* n8n
* Claude Code plugin commands and skills

## Next steps

> [card] [Scan your agent](https://docs.plurai.ai/guides/scan-your-agent.md)
>
> Generate the harness for your agent.

> [card] [Write a harness](https://docs.plurai.ai/sdk/write-a-harness.md)
>
> The `simulate.py` contract in detail.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.