Debug a failing test with an agent
This guide shows you how to get an agent to use a debugger on a failure that print statements are not explaining.
The debug tool drives a real Debug Adapter Protocol session. A stopped frame answers every question about that moment at once — every local, every caller, and an expression evaluated in the frame’s own scope — where a print statement answers only the question you thought to ask before running.
Before you start
Section titled “Before you start”The tool is offered only when an adapter is genuinely usable, which means both of:
- a project marker in the checkout, and
- the adapter’s binary resolving.
Ask the agent to check:
debug(op: "adapters")It answers with what it found, or explains that an adapter needs both halves.
| Language | Adapter | Marker | Install |
|---|---|---|---|
| Dart / Flutter | dart debug_adapter |
pubspec.yaml |
Ships with the SDK — nothing to do |
| Python | python3 -m debugpy.adapter |
pyproject.toml, setup.py, requirements.txt |
pip install debugpy |
| Rust / C / C++ | lldb-dap |
Cargo.toml, CMakeLists.txt |
Ships with LLVM |
| Go | dlv dap |
go.mod |
go install github.com/go-delve/delve/cmd/dlv@latest |
Set the breakpoint first, then launch
Section titled “Set the breakpoint first, then launch”The order matters and the tool enforces it: breakpoints may be set before a session exists, because that is the sequence the protocol requires.
Ask the agent something like:
The
computefunction inbin/main.dartreturns the wrong value. Set a breakpoint on the line that assignsproduct, launch it under the debugger and tell me whataandbactually are.
It will run roughly:
debug(op: "breakpoints", file: "bin/main.dart", lines: [7])debug(op: "launch", program: "bin/main.dart")The launch reports where it stopped:
Launched bin/main.dart on dart (session dart-1). Stopped: breakpoint. Use `stack` to see where.Read the frame
Section titled “Read the frame”debug(op: "stack")→ #1 compute (bin/main.dart:7:3) #2 main (bin/main.dart:2:18)Then walk into it:
debug(op: "scopes", frame_id: 1)→ Locals (reference 12)
debug(op: "variables", reference: 12)→ a (int) = 6 b (int) = 7 product (int) = 42An object with children reports [expand: reference N] — but only when there is genuinely something behind it, so an expansion the agent cannot use is never offered.
For anything not visible as a local:
debug(op: "evaluate", expression: "config.timeout.inSeconds", frame_id: 1)Step through
Section titled “Step through”| What you want | Op |
|---|---|
| Run to the next stop | continue |
| Next line, same frame | step_over |
| Into the call | step_in |
| Out to the caller | step_out |
| Stop a running program | pause |
Attach to something already running
Section titled “Attach to something already running”For a wedged process rather than one you are starting:
debug(op: "attach", port: 5858)Finish
Section titled “Finish”debug(op: "terminate")Always end the session. A debug adapter owns a stopped process holding whatever that process holds — a port, a lock, a database connection.
Bounded output
Section titled “Bounded output”A variables expansion on a deep object graph is unbounded by nature — one Flutter widget reaches the whole element tree. Children per scope are capped at 50, values at 400 characters and stack frames at 30. A tool result that fills the context window is a tool that ends the run it was helping.
Related
Section titled “Related”- Code intelligence — why a debugger, a compiler and a matcher answer different questions
- Built-in agent tools — every
debugop and argument - Diagnose an agent — when the problem is the agent, not the code