Luxe

Debugging in depth

The full DAP guide: adapters, breakpoints, and driving a session.

Luxe drives a real debugger over the Debug Adapter Protocol (DAP) through the debug tool, so the agent can set breakpoints, step, and inspect/modify live state instead of print-debugging. The tool is first-class and always in the model’s toolset; its description steers the model to prefer it over print-debugging, and Luxe reinforces that at runtime (see Closing the loop).

Adapters

Built-in (install the binary; Luxe knows how to launch + route it):

LanguageAdapterFiles
Pythondebugpy.py
Rust / C / C++CodeLLDB (or lldb-dap / gdb)compiled binaries
GoDelve.go

Add more without a code change in ~/.config/luxe/debug-adapters.json (a JSON array), reusing a built-in launch dialect and a transport:

[
  {
    "id": "ruby",
    "command": ["rdbg", "--open", "--port", "${DAP_ADDR}"],
    "extensions": ["rb"],
    "dialect": "debugpy",
    "transport": "tcp_dial_in"
  }
]
  • dialect — the launch-arg shape: debugpy | lldb | gdb | delve.
  • transportstdio (default) or tcp_dial_in (the adapter dials back into a loopback port Luxe binds; ${DAP_ADDR} is replaced with host:port).
  • extensions auto-routes matching files; any adapter is also selectable via the tool’s adapter argument.

Run /debug in the TUI to see configured adapters, which are installed, and any live sessions.

Workflow

launch (or attach) returns a session id; every follow-up call passes it. On each stop the result already includes the top frame + locals, so a breakpoint hit is immediately actionable.

  • Start: launch{program, args?, cwd?, breakpoints?, stopOnEntry?} or attach{pid | port}.
  • Breakpoints: set_breakpoints{session, path, lines, condition?}, set_function_breakpoints{session, functions}.
  • Run: continue | next | step_in | step_out | pause{session, threadId?}.
  • Inspect: threads | stack_trace{session}, scopes{session, frameId}, variables{session, variablesReference}, evaluate{session, expression, frameId?}.
  • Mutate / end: set_variable{session, variablesReference, name, value}, output | sessions | terminate{session}.

A worked example (breakpoint → inspect → fix)

// 1. Launch, breaking where the wrong value appears.
debug({ "action": "launch", "program": "target/debug/app",
        "breakpoints": [{ "path": "src/parse.rs", "lines": [42] }] })
// → stops at parse.rs:42, result shows the top frame + locals

// 2. Inspect the suspicious expression at the current frame.
debug({ "action": "evaluate", "session": "<id>", "expression": "tokens.len()" })

// 3. Step and re-inspect until the cause is clear, then edit the source and re-run.
debug({ "action": "next", "session": "<id>" })

Remote / already-running processes

Attach to a running process by pid (native) or to a listening adapter by port:

debug({ "action": "attach", "pid": 12345 })              // native (lldb/gdb)
debug({ "action": "attach", "port": 5678 })              // e.g. debugpy --listen 5678

Closing the loop

The debugger is wired into Luxe’s feedback loop so the model reaches for it at the right time:

  • Diagnostics on edit — after every edit/write, Luxe surfaces fresh LSP errors/warnings in the tool result (see Language intelligence), so many bugs are caught before they need a debugger.
  • Failure nudge — after 3 consecutive failing test runs, Luxe appends a one-time hint suggesting a breakpoint-and-inspect pass rather than more edit-and-retry (toggle LUXE_DEBUG_NUDGE=0).
  • DEBUG dock section — while a session is live, the Context Dock shows a DEBUG section with each session’s program and whether it’s running or stopped (with the stop reason). It’s hidden when nothing is being debugged.

Notes

  • Adapters are external binaries: a missing one degrades gracefully with an install hint; the agent can install it (via bash) and Luxe launches it on the next call.
  • Sessions are reused across tool calls and reaped on quit / project switch (bounded shutdown, then kill-on-drop), so a stuck adapter can’t linger.
  • Program output is captured byte-for-byte as the adapter sends it, so a partial line (print(..., end=""), a \r progress bar) reads the way it would in a terminal. Output events don’t align with line boundaries, so Luxe only records where the stream changed (stdout → stderr → adapter console) and breaks the display there — otherwise two unrelated messages glue into one nonsense token mid-line.

Capturing a TUI rendering fault

If the interface itself ever garbles (stray escape sequences, a half-drawn frame), the useful evidence is the byte stream, not a screenshot. Reproduce under a recorder:

script -f /tmp/luxe.tty -c luxe        # raw byte log, replay with `cat`/`scriptreplay`
asciinema rec /tmp/luxe.cast -c luxe   # if you prefer a shareable cast
RUST_LOG=luxe_tui=debug,luxe_core=debug luxe   # logs land in .luxe/logs/luxe.log

script keeps every byte Luxe wrote, which is what identifies the culprit — a debug adapter or subprocess writing to the terminal directly, a mis-sized frame, or an escape sequence Luxe emitted itself. Attach the log (and .luxe/logs/luxe.log for the same window) to the report.