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
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.LangGraph
LangGraph is the primary target. For a LangGraph agent the harness:- Patches tools before the graph is built.
plurai.apply_mocksrebinds each tool function in its module, soToolNode([get_user, process_refund])is constructed with the mocked functions.bind_toolsstill works, because the wrapper keeps each tool’sname,descriptionandargs_schema. - Can swap
ToolNodeforplurai.MockToolNode.MockToolNodeis a drop-in replacement that dispatches each tool call by its mode and returnsToolMessages that matchToolNode’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 withMemorySaver(a production checkpointer such asPostgresSaveris stubbed out). - Resumes interrupts on your agent’s own choice token. A turn that answers an
interrupt()carries the user’s choice inpayload["interrupt_choice"], and the harness resumes the graph withCommand(resume=choice).
simulate.py (excerpt)
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: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 understate_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.
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.