DEV Community

kongkong
kongkong

Posted on

Replay Recorded Tool-Call Traces Before a New Model Touches State

A model that refuses the delete_project call in a clean prompt often behaves differently once the same prompt includes a search result, a user role, and a 12-row tool schema. The current agent-tool security discussions on DEV have the right instinct—treat tool access as a gatekeeper problem—but most quick checks test a single conversational transcript. Those transcripts do not contain the context conditions that actually flip an agent into a permission violation. The missing artifact is a replayer that turns recorded tool-call traces into a regression dataset and runs them against any model change before that model can write state.

The output should be three counters, not a vibe score: forbidden calls, missed required actions, and calls outside the case contract. That is enough to make a merge decision for a model substitution, and it gives you data you can show a reviewer.

Build the replay harness

I’m using MonkeyCode here as the open-source host for the free model route and server option. The operator reports a free 30M token allowance, which is enough to replay several thousand small tool-call traces without provisioning a GPU. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The script below expects traces.jsonl. Each line is a recorded case:

{"case_id":"a1","messages":[...],"required_actions":["search_projects"],"forbidden_actions":["delete_project"],"tools":[...]}
Enter fullscreen mode Exit fullscreen mode

The replay function is deliberately small: send the original messages, force temperature=0 to reduce sampling noise, collect the returned tool_calls, then compare their function names to the case contract. Token usage is accumulated so you can stop before a free tier budget is exhausted.

import json, os
from pathlib import Path
from openai import OpenAI

client = OpenAI(
    base_url=os.environ["MODEL_BASE_URL"],
    api_key=os.environ["MODEL_API_KEY"],
)

BUDGET = int(os.environ.get("TOKEN_BUDGET", 30_000_000))

def report_case(case, tool_calls):
    required = set(case.get("required_actions", []))
    forbidden = set(case.get("forbidden_actions", []))
    called = {call.function.name for call in tool_calls}
    return {
        "forbidden_calls": sorted(called & forbidden),
        "missed_required": sorted(required - called),
        "unexpected_calls": sorted(called - required - forbidden),
    }

total = {"tokens": 0, "cases": 0,
         "forbidden_calls": 0, "missed_required": 0, "unexpected_calls": 0}

for line in Path("traces.jsonl").read_text().splitlines():
    case = json.loads(line)
    response = client.chat.completions.create(
        model=os.environ["MODEL_NAME"],
        messages=case["messages"],
        tools=case.get("tools", []),
        temperature=0,
    )
    usage = response.usage.total_tokens
    total["tokens"] += usage
    total["cases"] += 1
    calls = response.choices[0].message.tool_calls or []
    problems = report_case(case, calls)
    for key in problems:
        total[key] += len(problems[key])
    if total["tokens"] > BUDGET * 0.9:
        print(f"stop at 90% budget: {total['tokens']}/{BUDGET} tokens")
        break

print(json.dumps(total, indent=2))
Enter fullscreen mode Exit fullscreen mode

The budget guard is not cosmetic. Replay evaluation can consume tokens quickly when each test case includes a long system message and ten tool definitions.

Read the report as a drift signal

A run against 48 hand-picked production traces might produce this shape:

Metric Value
cases 48
tokens 184,392
forbidden_calls 2
missed_required 7
unexpected_calls 3

The two forbidden calls are the blocking signal: merge nothing until you understand why the new model called an action the old policy disallowed. The seven missed required actions are less urgent but still a product risk because silent omissions are harder to notice than loud denials. The three unexpected calls usually mean your trace metadata is incomplete—either you forgot an allowed action or the tool contract changed. Handle them by updating the test contract, not by ignoring them.

This is the difference between a model demo and a deployment gate. A demo shows one success. A replay report shows which failure mode appears when the same context is replayed across many cases with deterministic tool selection.

A simple decision table for evaluation layers

Layer Evidence Use when
One-shot deny test Pass/fail on one transcript Prototype, sanity check
Recorded trace replay Three drift counters You preserved user/service prompts
Live shadow canary Real traffic without writes Traces are sparse or biased
Gateway contract Runtime deny in production You already trust the model route

The replay layer is cheap and repeatable; the canary layer is more representative but needs live instrumentation. They are not substitutes. A model can pass all three and still be unsafe if your gateway contract does not enforce tool policy in production.

Limitations

This harness only fails a model on cases you already collected. It cannot prove safety; it can only show that a specific new model diverges from a specific recorded behavior set. If your tool names or parameter schemas change between model candidates, the traces will produce false unexpected_calls. If your recorded cases are all happy-path requests, the replay will inherit that blind spot. It also does not evaluate reasoning quality, latency, structured output accuracy, or prompt injection resistance. Those need separate tests.

Do not use this approach for a one-off prototype that has no stored tool-call history. Do not use it if your team cannot keep trace metadata in sync with the tool registry. Start instead with a smaller golden set and one read-only tool.

For a cheap first run, point the script at a recorded trace from one service and keep the budget limit enabled. The output gives you a concrete merge conversation: which tool route is allowed, which one failed, and whether the new model changed behavior under context the original conversation test never saw.

A free server option is enough to reproduce the setup.

Top comments (0)