Skip to main content
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.

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:
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 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 ToolMessages 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).
simulate.py (excerpt)
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:
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.
  • 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.

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

Scan your agent

Generate the harness for your agent.

Write a harness

The simulate.py contract in detail.