PAIPAI

Tool Stubbing

Replace selected tool bodies while keeping the agent contract, schemas, prompts, and visibility rules intact.

Tool stubs are useful when a test is not about the tool body itself. Runtime tests use them to isolate PAI wiring. Agent evals use them to isolate model decision-making from databases, external APIs, emails, queues, and destructive side effects.

Stub Selected Tools

stubTools(agent, { tools }) returns a copy of the agent with selected tool execute functions replaced.

import { stubTools } from "@pai/test-utils";
import { myAgent } from "./agent";

const searchAgencies = vi.fn(async ({ input }) => ({
  agencies: [
    {
      id: "agency-northstar",
      name: "Northstar Engineering",
      matchedCapability: input.capability,
    },
  ],
}));

const agent = stubTools(myAgent, {
  tools: {
    searchAgencies,
  },
});

The tool name, schema, description, prompt exposure, and visibility rules stay the same. Only the execution function changes.

Because the replacement is a normal Jest/Vitest mock, use the test framework to control it:

searchAgencies.mockResolvedValueOnce({ agencies: [] });
searchAgencies.mockClear();

expect(searchAgencies).toHaveBeenCalledWith(
  expect.objectContaining({
    input: { capability: "observability", region: "North America" },
  }),
);

Fail Unexpected Tool Calls

Use unhandled: "fail" when every tool call should be explicit in the test.

const agent = stubTools(myAgent, {
  unhandled: "fail",
  tools: {
    searchAgencies: async () => ({ agencies: [] }),
  },
});

If the model or mocked response calls any other tool, the test fails clearly. This is usually the right default for orchestration tests and evals.

The stub's failure is a deliberate ctx.fail, so the tool error names the tool it caught rather than the generic redacted message, under its own code:

expect(t.getTool(state, "approveShipment").error).toEqual({
  code: "tool_unexpected_call",
  message: "Unexpected tool call in test: approveShipment",
  retryable: false,
});

Keep Real Tools When They Matter

Do not stub a tool when the tool/runtime integration is the behavior under test. Keep the real tool and replace external dependencies the same way you would in ordinary backend tests: module mocks, fake repositories, sandbox clients, fixture data, or a prebuilt test agent.

import { testAgent } from "./agent.test-fixtures";

await using runtime = createTestRuntime({
  agent: testAgent,
  model,
});

The choice is simple: stub a tool when its body would add noise or risk; keep the real tool when its ToolData, metadata, suspend behavior, or service calls are part of what the test proves.

On this page