Skip to content

Recording

Zero-instrumentation with PatchSet

PatchSet patches the SDK classes directly so you do not need to change any agent code:

from agentsnap import PatchSet, AgentRecorder

with PatchSet():
    with AgentRecorder("my_agent") as rec:
        client = anthropic.Anthropic()    # raw client — no adapter
        result = my_agent(client, "What is Python?")
        rec.output = result

PatchSet covers all installed SDKs simultaneously. SDKs that are not installed are silently skipped.

In pytest — per test:

def test_my_agent(snapshot, agentsnap_instrument):
    with snapshot.run("my_agent") as s:
        client = anthropic.Anthropic()
        s.output = my_agent(client, "query")

In pytest — all tests in a session:

pytest --agentsnap-instrument

Project-wide (autouse in conftest.py):

# conftest.py
import pytest

@pytest.fixture(autouse=True)
def _(agentsnap_instrument):
    pass

Warning

Do not use PatchSet together with an adapter wrapper on the same client. Both interceptors will fire and events will be recorded twice, producing a corrupted trace. Use one or the other.

Using adapters (alternative)

Each supported provider has an adapter that wraps the SDK client explicitly. Use it everywhere you would use the raw client — useful when you want to be explicit about what is captured, or when you control the agent code directly:

import anthropic
from agentsnap.adapters.anthropic import AnthropicAdapter
from agentsnap.adapters.tool import ToolAdapter

# Wrap the client
client = AnthropicAdapter(anthropic.Anthropic())

# Wrap tools
def search(query: str) -> str:
    return f"Results for: {query}"

search_tool = ToolAdapter(search, name="search")

Now use client and search_tool exactly as you would use the unwrapped versions. The adapters are transparent pass-throughs when no recording context is active.

Available adapters:

from agentsnap.adapters.anthropic  import AnthropicAdapter
from agentsnap.adapters.openai     import OpenAIAdapter
from agentsnap.adapters.gemini     import GeminiAdapter
from agentsnap.adapters.cohere     import CohereAdapter
from agentsnap.adapters.mistral    import MistralAdapter
from agentsnap.adapters.groq       import GroqAdapter      # subclass of OpenAIAdapter
from agentsnap.adapters.openrouter import OpenRouterAdapter  # subclass of OpenAIAdapter
from agentsnap.adapters.langgraph  import LangGraphAdapter
from agentsnap.adapters.tool       import ToolAdapter

Recording with adapters:

from agentsnap import AgentRecorder

with AgentRecorder("my_agent", model="claude-haiku-4-5") as rec:
    rec.input_data = {"query": "What is Python?"}   # optional — stored in snapshot
    result = my_agent(client, search_tool, "What is Python?")
    rec.output = result

Asserting with adapters:

from agentsnap import AgentAsserter

with AgentAsserter("my_agent") as a:
    result = my_agent(client, search_tool, "What is Python?")
    a.output = result

If the trace matches the golden snapshot, the context manager exits cleanly. If anything drifted, it raises AgentRegressionError.

pytest fixture

The snapshot fixture is registered automatically when agentsnap is installed — no imports needed in your test files.

def test_agent(snapshot):
    # Auto mode (recommended): records on first run, asserts on every run after
    with snapshot.run("test_name") as s:
        s.output = my_agent(...)

    # Explicit record
    with snapshot.record_agent("test_name") as rec:
        rec.output = my_agent(...)

    # Explicit assert
    with snapshot.assert_agent("test_name") as a:
        a.output = my_agent(...)

Per-test overrides:

def test_agent(snapshot):
    # Tighter threshold for a critical agent
    with snapshot.assert_agent("name", semantic_threshold=0.98) as a:
        a.output = my_agent(...)

    # Force embeddings even when LLM judge is configured
    with snapshot.assert_agent("name", judge=False) as a:
        a.output = my_agent(...)

    # Ignore volatile fields in tool arguments
    with snapshot.assert_agent("name", ignored_fields=["timestamp", "request_id"]) as a:
        a.output = my_agent(...)

By default agentsnap places snapshots in __agent_snapshots__/ relative to the nearest conftest.py. To use a different location:

with AgentRecorder("name", snapshot_dir="/path/to/snapshots") as rec:
    ...

See Configuration for the full list of pytest flags and ini options.

Snapshot files and format

Golden snapshots live in __agent_snapshots__/ and are committed to git. The .last_run/ subdirectory is written on every assert run and is gitignored — it is only used by agentsnap update.

{
  "version": "1.1",
  "recorded_at": "2026-01-01T00:00:00+00:00",
  "model": "claude-haiku-4-5",
  "input": { "query": "What is Python?" },
  "trace": [
    { "step": 0, "type": "llm_call", "messages": [...], "response": "...", "tokens": 350, "raw_response": {...} },
    { "step": 1, "type": "tool_call", "name": "search", "args": {"query": "Python"}, "result": "..." }
  ],
  "output": "Python is a high-level programming language..."
}

Scenarios and input binding

Multiple snapshots per test function are namespaced by a scenario name. If no explicit scenario is given, agentsnap derives one automatically from an input_sha8 hash of whatever you assign to rec.input_data / a.input:

with AgentRecorder("my_agent", scenario="short_question") as rec:
    ...

If you don't set input_data/input and don't pass scenario=, all runs of that test share a single unnamed snapshot. agentsnap update <test_name> promotes all scenario variants for a test at once (wildcard), not just one.

When the input captured inside the with block differs from what was recorded, AgentAsserter prints a warning (comparison may be against the wrong baseline) rather than failing silently.