Agents and tool calls
counterfix checks how a model uses tools, on a single call that offers tools and on a whole agent run, and reports F14 tool misuse with its cause.
Single calls: did it pick the right tool?
Give each item the tools and the call you expect:
{"prompt": "What's the weather in Pune?", "tools": [...],
"reference": {"name": "get_weather", "arguments": {"city": "Pune"}}}
counterfix eval tool_tests.jsonl --model groq:openai/gpt-oss-120b --metric tool_call --fix 5
counterfix benchmark tools --model ollama:qwen2.5:3b --fix 10
The tool_call metric compares the call with the reference and names the cause of a miss:
| Cause | Meaning |
|---|---|
no_call |
no tool call when one was expected |
extra_call |
a tool call when the reference is "no_call" (no tool needed) |
wrong_tool |
another offered tool was called |
unknown_tool |
a tool that was not offered was called |
bad_arguments |
the right tool, with a missing or wrong argument, or arguments that are not JSON |
Strings compare without case and outer spaces, numbers by value, and an argument can
accept several values: {"city": {"any_of": ["Pune", "Pune, India"]}}. Arguments the
reference does not name are allowed. A list of calls means all of them, in any order
(parallel calls).
The tools probe set has 20 probes over one toolbox of 9 tools: plain calls, choices
between two similar tools (weather now or forecast), argument formats (ISO dates, currency
codes) and requests that need no tool, in the style of the Berkeley Function Calling
Leaderboard's simple and irrelevance categories. They are easy (qwen2.5:3b scores 20/20):
use them as a smoke test for your tool setup, not to rank models.
Agent runs: loops, unknown tools, missing calls, task success
Turn a run into a record with agents.record(task, run) and diagnose it, or
return the run from your app in evaluate(app=...):
import counterfix
from counterfix.detect import agents
result = my_agent.invoke({"messages": [("user", task)]}) # LangGraph
rec = agents.record(task, result, tools=my_tools, expect_tools=["search_web"])
print(counterfix.diagnose(rec).summary())
run can be an OpenAI-style message list (assistant tool_calls, tool results), a
LangChain or LangGraph message list or state {"messages": [...]}, Anthropic messages
(tool_use / tool_result blocks), OpenTelemetry GenAI spans (execute_tool spans), or
one guarded response or CallRecord per model call.
Free live checks (ToolUseCheck, no extra calls):
unknown_tool: a call to a tool that was not offered;loop: the same call (name and arguments) 3 times, or twice in a row with the same result;missing_call: a tool inexpect_tools(a dataset field too) was never called;too_many_steps: more model turns thanmax_steps.
With a judge, AgentTaskCheck reads the run and decides whether it completed the task,
and if not, why: wrong_tool, bad_arguments, ignored_result (the answer contradicts a
tool result, including an error), missing_step, gave_up, wrong_answer.
Fixes it verifies
On single calls, each fix is re-run and kept only if the call is right clearly more often than on a plain retry (how):
| Intervention | Change |
|---|---|
tool_choice |
force the right tool when the reference names it, any tool when one was needed, none when none was |
name_the_tool |
say in the system prompt which tool the request needs (or that it needs none) |
describe_arguments |
spell out each tool's arguments (type, required, allowed values) in its description |
no_repeat |
tell the model never to repeat a call with the same arguments |
For agent runs, the findings carry a prescription; re-running a whole agent with a change needs your agent's code, so counterfix verifies fixes on single calls only.