ToolMaze

June 5, 2026 · View on GitHub

This project is from the paper "When Tools Fail: Benchmarking Dynamic Replanning and Anomaly Recovery in LLM Agents ". Evaluation framework for testing LLM agents on C1/C2/C3/C4 tool-use tasks under P0–P4 perturbation modes. The framework runs an agent inside a sandboxed tool runtime, intercepts tool calls to inject perturbations, and scores the result with a complexity-aware judge.

Project Layout

ToolMaze/
├── toolmaze/              # Core runtime (ExecutionContext, ToolExecutor, ...)
├── tools/                 # Tool plugins (273 plugins) and YAML definitions
│   ├── definitions/       # Tool spec YAMLs (Source / Processor / Action)
│   └── plugins/           # Executable plugin modules
├── data/
│   ├── perturbed_tasks/   # Task JSONs grouped by complexity (c1..c4)
|   ├── tasks/             # Task JSONs generated from templates
│   └── templates/         # DAG templates used by the judge
├── evaluation/
│   ├── core/              # sandbox.py, judge.py, metrics.py
│   ├── agents/            # OpenAI / Anthropic / VLLM / MCP agents
│   ├── utils/             # trace_logger, result_saver
│   ├── configs/           # YAML configs
│   ├── scripts/           # run_eval.py, find_failed_inferences.py
│   └── results/           # Output directory (auto-populated)
├── run_api_eval.py        # Single-model API-based runner
├── run_batch_api_eval.py  # Multi-model concurrent runner with retry loop
└── requirements.txt

Quick Start

1. Install dependencies

conda create -n toolmaze python=3.10 -y
conda activate toolmaze
pip install -r requirements.txt

2. Configure API credentials

Edit evaluation/configs/openai_eval_config_c1.yaml:

agent:
  type: "openai"          # or "anthropic", "vllm", "mcp"
  model: "gpt-4o"
  api_key: "${OPENAI_API_KEY}"
  base_url: "https://api.openai.com/v1"

judge:
  model: "gpt-4o"
  api_key: "${OPENAI_API_KEY}"
  base_url: "https://api.openai.com/v1"
export OPENAI_API_KEY="your-key"

3. Download dataset

You can download the dataset from this link and unzip it into data.

4. Run evaluation

Single task / category:

# Single task (category auto-inferred from task ID)
python evaluation/scripts/run_eval.py --task-id C1_task_001_P0

# All P0 tasks in c1
python evaluation/scripts/run_eval.py --modes P0

# Full c3 sweep
python evaluation/scripts/run_eval.py --task-category c3 --modes P0 P1 P2 P3 P4

# Override agent + model from CLI
python evaluation/scripts/run_eval.py --agent-type openai --model gpt-4o --modes P0 P1

Single model, all categories (uses evaluation/configs/openai_eval_config_c1.yaml as the base config and overrides agent fields):

python run_api_eval.py --models gpt-4o
python run_api_eval.py --models gpt-4o --categories c3 c4
python run_api_eval.py --models gpt-4o --modes P2 P4

Concurrent batch over multiple models (with delete-and-retry):

Edit the MODELS dict at the top of run_batch_api_eval.py, then:

python run_batch_api_eval.py
python run_batch_api_eval.py --categories c3 c4
python run_batch_api_eval.py --modes P2 P4

Output Files

Results are saved under evaluation/results/{model}/{fc|mcp}/{c1|c2|c3|c4}/:

inferences/{task_id}_{mode}_inference.json   # Full message trace + token usage
evaluations/{task_id}_{mode}_eval.json       # Judge verdict + reasoning
metrics/metrics_report_{timestamp}.json      # Aggregated TSR / PRR / RC
reports/                                     # Optional text reports

fc = native function calling agents (openai / anthropic / vllm). mcp = prompt-based MCP-style tool calling.

Metrics

TSR (Task Success Rate)

TSR_mode = Count(Pass runs in mode) / Total runs in mode

PRR (Perturbation Recovery Rate)

PRR_mode = Count(Hit and recover in mode) / Count(Hit in mode)

Hit = the run actually touched a perturbed tool call (metadata.perturbation_status == "perturbed" in the saved trace).

RC (Recovery Cost)

For each run r in mode:
  RC_r = 0,                                   if r does not hit the perturbation
  RC_r = 1,                                   if r hits but fails to recover
  RC_r = 1 - C_oracle(r) / max(C_act, C_oracle), if r hits and recovers

RC_mode = mean(RC_r over runs in mode)
  • C_act — number of tool-call results from the first perturbed hit to end of run.
  • C_oracle — minimum recovery suffix cost derived from the task structure.
  • RC ∈ [0, 1]. No-hit runs contribute 0, hit-and-fail runs contribute 1.

How Perturbation Injection Works

evaluation/core/sandbox.py wraps the tool runtime with an ExecutionEngine:

  • C1: a single perturbation point per task.
  • C2 / C3: per-path perturbation maps, activated via first-touch on an alternative tool.
  • C4: per-slot first-touch — each slot's first-called tool becomes the victim for that slot.
  • P1 / P3: transient perturbation (only the first call to the victim returns the corrupted output).
  • P2 / P4: persistent perturbation (every call returns corrupted output).

The trace logger records every round, tool call, perturbation status, and token count so the judge can score recovery behaviour.

Adding a new agent backend

Implement evaluation/agents/<name>_agent.py exposing a class that inherits BaseAgent (see base_agent.py), then register it in evaluation/agents/__init__.py and add a branch in evaluation/scripts/run_eval.py:create_agent().

Citation

@misc{zhu2026toolsfailbenchmarkingdynamic,
      title={When Tools Fail: Benchmarking Dynamic Replanning and Anomaly Recovery in LLM Agents}, 
      author={Dongsheng Zhu and Xuchen Ma and Yucheng Shen and Xiang Li and Yukun Zhao and Shuaiqiang Wang and Lingyong Yan and Dawei Yin},
      year={2026},
      eprint={2606.05806},
      archivePrefix={arXiv},
      primaryClass={cs.AI},
      url={https://arxiv.org/abs/2606.05806}, 
}