Skip to content

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.

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

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 compute function in bin/main.dart returns the wrong value. Set a breakpoint on the line that assigns product, launch it under the debugger and tell me what a and b actually 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.
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) = 42

An 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)
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

For a wedged process rather than one you are starting:

debug(op: "attach", port: 5858)
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.

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.