agent-dbg 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +27 -1
- package/TODO.md +299 -0
- package/demo/DEMO.md +71 -0
- package/demo/order-processor.js +35 -0
- package/dist/main.js +1250 -187
- package/package.json +3 -1
- package/src/commands/attach.ts +2 -1
- package/src/commands/break-fn.ts +41 -0
- package/src/commands/launch.ts +2 -1
- package/src/daemon/client.ts +1 -1
- package/src/daemon/entry.ts +83 -45
- package/src/daemon/server.ts +67 -34
- package/src/daemon/session-breakpoints.ts +2 -1
- package/src/daemon/session-mutation.ts +1 -0
- package/src/daemon/session.ts +7 -5
- package/src/daemon/spawn.ts +21 -5
- package/src/dap/client.ts +252 -0
- package/src/dap/session.ts +1151 -0
- package/src/main.ts +1 -0
- package/src/protocol/messages.ts +12 -0
- package/tests/fixtures/dap/hello +0 -0
- package/tests/fixtures/dap/hello.c +8 -0
- package/tests/fixtures/dap/hello.dSYM/Contents/Info.plist +20 -0
- package/tests/fixtures/dap/hello.dSYM/Contents/Resources/DWARF/hello +0 -0
- package/tests/fixtures/dap/hello.dSYM/Contents/Resources/Relocations/aarch64/hello.yml +5 -0
- package/tests/fixtures/hotpatch-active-fn.js +7 -0
- package/tests/integration/daemon-logging.test.ts +8 -9
- package/tests/integration/mutation.test.ts +33 -0
- package/tests/unit/daemon-logger.test.ts +2 -2
- package/bun.lock +0 -60
|
@@ -19,7 +19,33 @@
|
|
|
19
19
|
"Bash(find:*)",
|
|
20
20
|
"Bash(xargs sed:*)",
|
|
21
21
|
"Bash(ps:*)",
|
|
22
|
-
"Bash(ls:*)"
|
|
22
|
+
"Bash(ls:*)",
|
|
23
|
+
"WebSearch",
|
|
24
|
+
"Bash(lldb-dap:*)",
|
|
25
|
+
"WebFetch(domain:microsoft.github.io)",
|
|
26
|
+
"WebFetch(domain:github.com)",
|
|
27
|
+
"Bash(lldb:*)",
|
|
28
|
+
"Bash(xcode-select:*)",
|
|
29
|
+
"Bash(npx biome check:*)",
|
|
30
|
+
"Bash(brew install:*)",
|
|
31
|
+
"Bash(cc:*)",
|
|
32
|
+
"Bash(git add:*)",
|
|
33
|
+
"Bash(git reset:*)",
|
|
34
|
+
"Bash(git commit:*)",
|
|
35
|
+
"Bash(xcrun simctl:*)",
|
|
36
|
+
"Bash(kill:*)",
|
|
37
|
+
"Bash(socat:*)",
|
|
38
|
+
"Bash(for tag in 4 8 10 14 16 18)",
|
|
39
|
+
"Bash(do echo \"=== Tag $tag ===\")",
|
|
40
|
+
"Bash(done)",
|
|
41
|
+
"Bash(for ref in @v12 @v13 @v14 @v15 @v16 @v17)",
|
|
42
|
+
"Bash(do echo \"=== $ref ===\")",
|
|
43
|
+
"Bash(for ref in @v18 @v19 @v20 @v21 @v22 @v23)",
|
|
44
|
+
"Bash(for ref in @v34 @v35 @v36 @v37 @v38 @v39)",
|
|
45
|
+
"Bash(for ref in @v322 @v343 @v364)",
|
|
46
|
+
"Bash(for ref in @v365 @v366 @v367)",
|
|
47
|
+
"Bash(pgrep:*)",
|
|
48
|
+
"Bash(node:*)"
|
|
23
49
|
]
|
|
24
50
|
}
|
|
25
51
|
}
|
package/TODO.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# TODO — Improvements from React Native Crash Debugging
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
While debugging a React Native iOS crash (SIGABRT in Yoga C++ caused by `width: 'fit-content'` in a StyleSheet), we used agent-dbg with `--runtime lldb` (DAP mode) to attach to the running app, catch the exception, and walk the stack to identify the problematic component.
|
|
6
|
+
|
|
7
|
+
The full workflow was:
|
|
8
|
+
1. `agent-dbg attach --runtime lldb <pid>` to connect to the RN app
|
|
9
|
+
2. `agent-dbg catch all` to break on exceptions
|
|
10
|
+
3. Trigger the crash in the app (toggle theme)
|
|
11
|
+
4. `agent-dbg stack` to get 50 frames from `__pthread_kill` through Yoga to unistyles
|
|
12
|
+
5. `agent-dbg vars --frame @fN` + `agent-dbg props @vN` repeated many times to drill into `ShadowNodeFamily` objects and find which component had the bad style
|
|
13
|
+
|
|
14
|
+
This revealed several friction points, ordered by impact for AI agent usage.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## High Impact
|
|
19
|
+
|
|
20
|
+
### 1. Deep object inspection (`props --depth N`)
|
|
21
|
+
|
|
22
|
+
**Issue:** Inspecting a nested object graph requires one `props` call per level. To identify which of 6 React Native nodes had the bad style, we needed ~20 sequential commands: `props @v386` → find entry → `props @v40` → find ShadowNodeFamily → `props @v41` → find `componentName_`. Multiply by 6 nodes. Each call is a full daemon round-trip, burning tokens and time for an AI agent.
|
|
23
|
+
|
|
24
|
+
**Example:**
|
|
25
|
+
```bash
|
|
26
|
+
# Current: 4 round-trips to reach componentName_
|
|
27
|
+
agent-dbg props @v386 # tagToProps map → 6 entries
|
|
28
|
+
agent-dbg props @v40 # entry 0 → {first: 4, second: @v41}
|
|
29
|
+
agent-dbg props @v41 # second → ShadowNodeFamily object
|
|
30
|
+
agent-dbg props @v42 # ShadowNodeFamily → {tag_: 4, componentName_: "View", ...}
|
|
31
|
+
|
|
32
|
+
# Desired: 1 round-trip
|
|
33
|
+
agent-dbg props @v386 --depth 3
|
|
34
|
+
# Returns the full tree expanded 3 levels deep
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Suggested fix:** Add `--depth N` (default 1) to `props` and `vars` commands. In `getProps()`, after fetching immediate children, recursively call `getProps()` on any child that has a `variablesReference > 0` (DAP) or is an object type (CDP), up to the depth limit. Cap at depth 5 to avoid runaway expansion. Return a nested JSON structure instead of a flat list.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### 2. Watchpoints / data breakpoints
|
|
42
|
+
|
|
43
|
+
**Issue:** DAP supports `dataBreakpoints` — break when a specific memory address is written to. This would directly answer "when does this variable get set to the bad value?" instead of the multi-breakpoint stepping approach we tried (set BP at clone loop, step through each clone, check props each time). LLDB natively supports watchpoints (`watchpoint set variable foo`).
|
|
44
|
+
|
|
45
|
+
**Example:**
|
|
46
|
+
```bash
|
|
47
|
+
# After finding the suspicious variable:
|
|
48
|
+
agent-dbg vars --frame @f288
|
|
49
|
+
# @v50 tagToProps size=6
|
|
50
|
+
|
|
51
|
+
# Desired: break when a specific tag's props are modified
|
|
52
|
+
agent-dbg watch @v50 --write
|
|
53
|
+
# → WP#1 set on tagToProps (0x7ff8a0012340), break on write
|
|
54
|
+
|
|
55
|
+
# Continue execution → breaks exactly when the bad style is applied
|
|
56
|
+
agent-dbg continue
|
|
57
|
+
# Paused: watchpoint WP#1 hit — tagToProps modified at ShadowTreeManager.cpp:34
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Suggested fix:** Add a `watch` command that sends DAP `dataBreakpointInfo` (to check if a variable supports data breakpoints) followed by `setDataBreakpoints`. Track watchpoints in a `watchpoints` array similar to `functionBreakpoints`. For CDP mode, this isn't natively supported — return an error with suggestion.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### 3. Loaded modules / libraries
|
|
65
|
+
|
|
66
|
+
**Issue:** We spent ~30 minutes trying to eval C++ expressions at React framework frames (`this->tag_`, `this->getComponentName()`) before realizing the framework binary had no debug symbols (built on CI, `.o` files missing). There was no way to know upfront which frames had usable debug info and which didn't.
|
|
67
|
+
|
|
68
|
+
**Example:**
|
|
69
|
+
```bash
|
|
70
|
+
# Current: blind trial-and-error
|
|
71
|
+
agent-dbg eval 'this->tag_' --frame @f300
|
|
72
|
+
# Error: invalid use of 'this' outside of a non-static member function
|
|
73
|
+
# (Why? No debug symbols — but the error message doesn't say that)
|
|
74
|
+
|
|
75
|
+
# Desired: check what has symbols first
|
|
76
|
+
agent-dbg modules
|
|
77
|
+
# MODULE SYMBOLS PATH
|
|
78
|
+
# MyApp full /path/to/MyApp.app/MyApp
|
|
79
|
+
# React.framework stripped /path/to/React.framework/React
|
|
80
|
+
# hermes.framework full /path/to/hermes.framework/hermes
|
|
81
|
+
# react-native-unistyles.framework full /path/to/unistyles.framework/...
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Suggested fix:** Add a `modules` command that sends DAP `modules` request. Display module name, symbol status (full/stripped/none), version, and path. Optionally filter: `agent-dbg modules --filter yoga`. For CDP, return the loaded scripts list (already available via `getScripts`).
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### 4. Attach by process name
|
|
89
|
+
|
|
90
|
+
**Issue:** Every debugging session required manually finding the PID (`xcrun simctl spawn booted launchctl list | grep MyApp`, or `pgrep -f MyApp`). DAP's attach request accepts a process `name` field, but we only exposed `target` (which expects a PID or WebSocket URL).
|
|
91
|
+
|
|
92
|
+
**Example:**
|
|
93
|
+
```bash
|
|
94
|
+
# Current: manual PID lookup
|
|
95
|
+
pgrep -f "debug-rn-wrong-dimensions" # → 12345
|
|
96
|
+
agent-dbg attach --runtime lldb 12345
|
|
97
|
+
|
|
98
|
+
# Desired: attach by name
|
|
99
|
+
agent-dbg attach --runtime lldb --name "debug-rn-wrong-dimensions"
|
|
100
|
+
# Attached to PID 12345 (debug-rn-wrong-dimensions)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Suggested fix:** In `DapSession.attach()`, detect if `target` is numeric (PID) or a string (process name). If string, pass `{ name: target }` instead of `{ pid: parseInt(target) }` in the DAP attach request. LLDB-DAP supports both. For CDP, keep existing behavior (target is always a WebSocket URL or port).
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### 5. `run-to` for DAP
|
|
108
|
+
|
|
109
|
+
**Issue:** `run-to` is not implemented for DAP sessions. We had to manually set a breakpoint, continue, then remove it — 3 commands instead of 1. This pattern was used repeatedly in the investigation scripts.
|
|
110
|
+
|
|
111
|
+
**Example:**
|
|
112
|
+
```bash
|
|
113
|
+
# Current: 3 commands
|
|
114
|
+
agent-dbg break ShadowTreeManager.cpp:22
|
|
115
|
+
# BP#5 set
|
|
116
|
+
agent-dbg continue
|
|
117
|
+
# Paused at ShadowTreeManager.cpp:22
|
|
118
|
+
agent-dbg break-rm BP#5
|
|
119
|
+
|
|
120
|
+
# Desired: 1 command
|
|
121
|
+
agent-dbg run-to ShadowTreeManager.cpp:22
|
|
122
|
+
# Paused at ShadowTreeManager.cpp:22 (temporary breakpoint auto-removed)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Suggested fix:** Implement `DapSession.runTo(file, line)` by: (1) setting a temporary breakpoint via `setBreakpoints` (add to existing file breakpoints), (2) calling `continue`, (3) on stop, removing the temporary breakpoint. Alternatively, some DAP adapters support `goto` targets — check adapter capabilities first.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Medium Impact
|
|
130
|
+
|
|
131
|
+
### 6. Disassembly view
|
|
132
|
+
|
|
133
|
+
**Issue:** When source code isn't available (framework built on CI without debug symbols), the `source` command returns nothing useful. Disassembly is the only way to understand what's executing. DAP has a `disassemble` request that returns instruction-level code, but we never exposed it.
|
|
134
|
+
|
|
135
|
+
**Example:**
|
|
136
|
+
```bash
|
|
137
|
+
# Current: no source available
|
|
138
|
+
agent-dbg source --frame @f300
|
|
139
|
+
# Error: no source available for this frame
|
|
140
|
+
|
|
141
|
+
# Desired: fall back to disassembly
|
|
142
|
+
agent-dbg disassemble --frame @f300
|
|
143
|
+
# 0x1a2b3c40 stp x29, x30, [sp, #-16]!
|
|
144
|
+
# 0x1a2b3c44 mov x29, sp
|
|
145
|
+
# 0x1a2b3c48 bl 0x1a2b4000 ; yoga::Style::operator==
|
|
146
|
+
# → 0x1a2b3c4c cbz w0, 0x1a2b3c60 ; <-- current instruction
|
|
147
|
+
# 0x1a2b3c50 ldr x8, [x19, #24]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Suggested fix:** Add a `disassemble` command that sends DAP `disassemble` request with the current frame's `instructionPointerReference`. Format output with addresses, mnemonics, and an arrow marking the current instruction. For CDP, this isn't applicable — return an error.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### 7. Memory read
|
|
155
|
+
|
|
156
|
+
**Issue:** We couldn't inspect `folly::dynamic` internals because LLDB's expression evaluator didn't understand F14 hash maps. But with raw memory access + known struct layouts, we could have manually decoded the data. DAP has `readMemory` request.
|
|
157
|
+
|
|
158
|
+
**Example:**
|
|
159
|
+
```bash
|
|
160
|
+
# Current: opaque object
|
|
161
|
+
agent-dbg eval '*(folly::dynamic::ObjectImpl*)(0x7ff8a0012340)'
|
|
162
|
+
# Error: no member named 'ObjectImpl' in namespace 'folly::dynamic'
|
|
163
|
+
|
|
164
|
+
# Desired: read raw memory
|
|
165
|
+
agent-dbg memory 0x7ff8a0012340 --count 128
|
|
166
|
+
# 0x7ff8a001_2340: 06 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 ................
|
|
167
|
+
# 0x7ff8a001_2350: 40 56 01 a0 f8 7f 00 00 00 00 00 00 00 00 00 00 @V..............
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Suggested fix:** Add a `memory` command that sends DAP `readMemory` request. Accept address (hex) and byte count. Format as hex dump with ASCII sidebar. For CDP, not applicable.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
### 8. Exception breakpoint filters
|
|
175
|
+
|
|
176
|
+
**Issue:** Our `catch` command only supports `all/none/uncaught`. When debugging the RN crash, `catch all` caught every signal (including harmless SIGTRAPs from thread creation), requiring manual `continue` past irrelevant stops. DAP supports filter-based exception breakpoints — LLDB can catch only `SIGABRT`.
|
|
177
|
+
|
|
178
|
+
**Example:**
|
|
179
|
+
```bash
|
|
180
|
+
# Current: catch everything
|
|
181
|
+
agent-dbg catch all
|
|
182
|
+
# Stops on SIGTRAP (thread created) — irrelevant, continue
|
|
183
|
+
# Stops on SIGTRAP (breakpoint) — irrelevant, continue
|
|
184
|
+
# Stops on SIGABRT — the actual crash
|
|
185
|
+
|
|
186
|
+
# Desired: catch specific signals
|
|
187
|
+
agent-dbg catch --filter SIGABRT
|
|
188
|
+
# Only stops on SIGABRT
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Suggested fix:** Query the adapter's `exceptionBreakpointFilters` capability (returned in `initialize` response) and expose them. Add `--filter <name>` flag to `catch` command. Send `setExceptionBreakpoints` with selected filter IDs instead of blanket all/none.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### 9. Restart for DAP
|
|
196
|
+
|
|
197
|
+
**Issue:** After each investigation attempt, we had to `stop` the session and re-launch/re-attach manually. `restart` isn't implemented for DAP.
|
|
198
|
+
|
|
199
|
+
**Example:**
|
|
200
|
+
```bash
|
|
201
|
+
# Current: 2 commands + re-specify args
|
|
202
|
+
agent-dbg stop
|
|
203
|
+
agent-dbg attach --runtime lldb 12345
|
|
204
|
+
|
|
205
|
+
# Desired: 1 command
|
|
206
|
+
agent-dbg restart
|
|
207
|
+
# Re-attached to PID 12345
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Suggested fix:** Store the original launch/attach arguments in `DapSession`. On `restart()`, call `stop()` then replay the original `launch()` or `attach()` call. Some DAP adapters also support the `restart` request natively — check capabilities first.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### 10. Stack filtering
|
|
215
|
+
|
|
216
|
+
**Issue:** The crash produced 50 stack frames, but only ~5 were relevant (yoga layout, unistyles shadow tree update). An AI agent has to parse all 50 to find the interesting ones, wasting tokens.
|
|
217
|
+
|
|
218
|
+
**Example:**
|
|
219
|
+
```bash
|
|
220
|
+
# Current: 50 frames, most are system/framework noise
|
|
221
|
+
agent-dbg stack
|
|
222
|
+
# @f0 __pthread_kill (libsystem_kernel.dylib)
|
|
223
|
+
# @f1 pthread_kill (libsystem_pthread.dylib)
|
|
224
|
+
# ... 45 more framework frames ...
|
|
225
|
+
# @f47 -[UIApplication sendAction:to:from:forEvent:]
|
|
226
|
+
# @f48 UIApplicationMain
|
|
227
|
+
|
|
228
|
+
# Desired: filter by keyword
|
|
229
|
+
agent-dbg stack --filter yoga
|
|
230
|
+
# @f5 yoga::StyleValuePool::getLength (yoga.cpp:1234)
|
|
231
|
+
# @f6 yoga::Node::setStyle (Node.cpp:567)
|
|
232
|
+
# @f8 facebook::react::updateYogaProps (Props.cpp:89)
|
|
233
|
+
|
|
234
|
+
# Or show only user code (non-system frames)
|
|
235
|
+
agent-dbg stack --user
|
|
236
|
+
# @f10 margelo::nitro::unistyles::HybridStyleSheet::onPlatformDependenciesChange
|
|
237
|
+
# @f12 margelo::nitro::unistyles::ShadowTreeManager::updateShadowTree
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Suggested fix:** Add `--filter <keyword>` flag to `stack` command that filters frames by function name substring match. Add `--user` flag that excludes frames from system libraries (heuristic: exclude paths containing `/usr/lib/`, `libsystem_`, `UIKit`, `CoreFoundation`, etc.). Both are client-side filters on the existing stack data — no protocol changes needed.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Low Impact
|
|
245
|
+
|
|
246
|
+
### 11. Toggle breakpoint for DAP
|
|
247
|
+
|
|
248
|
+
**Issue:** `break-toggle` throws "not yet supported" for DAP. DAP tracks enabled/disabled state per breakpoint — when re-sending `setBreakpoints`, each breakpoint can have an `enabled` field set to `false`.
|
|
249
|
+
|
|
250
|
+
**Example:**
|
|
251
|
+
```bash
|
|
252
|
+
agent-dbg break-toggle BP#3
|
|
253
|
+
# BP#3 disabled (was enabled)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Suggested fix:** Add an `enabled` field to `DapBreakpointEntry`. On toggle, flip the flag and re-sync breakpoints for that file via `setBreakpoints`, including `enabled: false` for the toggled one.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
### 12. Loaded sources for DAP
|
|
261
|
+
|
|
262
|
+
**Issue:** `scripts` returns an empty array for DAP sessions. DAP has a `loadedSources` request that returns all source files known to the debugger.
|
|
263
|
+
|
|
264
|
+
**Example:**
|
|
265
|
+
```bash
|
|
266
|
+
agent-dbg scripts --filter unistyles
|
|
267
|
+
# (empty — no scripts tracked in DAP mode)
|
|
268
|
+
|
|
269
|
+
# Desired:
|
|
270
|
+
# ShadowTreeManager.cpp /path/to/unistyles/ShadowTreeManager.cpp
|
|
271
|
+
# HybridStyleSheet.cpp /path/to/unistyles/HybridStyleSheet.cpp
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Suggested fix:** Implement `DapSession.getScripts(filter?)` by sending DAP `loadedSources` request. Cache the result (it doesn't change often). Apply the filter client-side on the returned source names/paths.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
### 13. Better eval error suggestions
|
|
279
|
+
|
|
280
|
+
**Issue:** When `eval` fails, the error message comes raw from the debugger with no actionable suggestions. For example, `this->tag_` fails with "invalid use of 'this' outside of a non-static member function" — but doesn't tell you *why* (no debug symbols) or *what to try instead*.
|
|
281
|
+
|
|
282
|
+
**Example:**
|
|
283
|
+
```bash
|
|
284
|
+
# Current:
|
|
285
|
+
agent-dbg eval 'this->tag_' --frame @f300
|
|
286
|
+
# Error: invalid use of 'this' outside of a non-static member function
|
|
287
|
+
|
|
288
|
+
# Desired:
|
|
289
|
+
agent-dbg eval 'this->tag_' --frame @f300
|
|
290
|
+
# Error: invalid use of 'this' outside of a non-static member function
|
|
291
|
+
# -> This frame may lack debug symbols. Try 'agent-dbg modules' to check.
|
|
292
|
+
# -> Try accessing the variable directly: 'tag_'
|
|
293
|
+
# -> Try a different frame: 'agent-dbg eval 'this->tag_' --frame @f301'
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Suggested fix:** In the eval error handler, pattern-match common LLDB error messages and append contextual suggestions. Examples:
|
|
297
|
+
- "invalid use of this" → suggest checking modules for symbols, try variable directly
|
|
298
|
+
- "no member named" → suggest `props` to list available members
|
|
299
|
+
- "use of undeclared identifier" → suggest `vars` to see what's in scope
|
package/demo/DEMO.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Demo: AI Agent Debugging with agent-dbg
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This demo shows Claude Code autonomously debugging a buggy Node.js app using `agent-dbg` — finding root causes, inspecting variables, and hot-patching fixes **without restarting the process**.
|
|
6
|
+
|
|
7
|
+
**Total demo time: ~3 minutes**
|
|
8
|
+
|
|
9
|
+
## The Buggy App
|
|
10
|
+
|
|
11
|
+
`order-processor.js` processes customer orders, applies discount codes, and calculates totals. It has **two bugs**:
|
|
12
|
+
|
|
13
|
+
| Bug | Type | Symptom |
|
|
14
|
+
|-----|------|---------|
|
|
15
|
+
| **Bug 1** — `calculateSubtotal` loop uses `<=` instead of `<` | Off-by-one | Crashes with `TypeError: Cannot read properties of undefined` |
|
|
16
|
+
| **Bug 2** — `applyDiscount` returns `subtotal * rate` instead of `subtotal * (1 - rate)` | Logic error | Returns the discount amount ($20) instead of the discounted price ($80) |
|
|
17
|
+
|
|
18
|
+
### Expected correct output
|
|
19
|
+
```
|
|
20
|
+
ORD-001: subtotal $125, total $100 (125 * 0.80)
|
|
21
|
+
ORD-002: subtotal $75, total $75 (no discount)
|
|
22
|
+
ORD-003: subtotal $225, total $180 (225 * 0.80)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Running the Demo
|
|
26
|
+
|
|
27
|
+
### Prompt to give Claude Code
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
This order processor is crashing and the totals look wrong. Debug it with agent-dbg and fix both bugs without restarting the process.
|
|
31
|
+
|
|
32
|
+
The app is at demo/order-processor.js
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### What the agent will do (key moments to narrate)
|
|
36
|
+
|
|
37
|
+
1. **Launch with `--brk`** — Agent starts the app paused at the first line using `agent-dbg launch --brk demo/order-processor.js`
|
|
38
|
+
|
|
39
|
+
2. **Set breakpoints** — Agent sets a breakpoint inside `calculateSubtotal` and continues execution
|
|
40
|
+
|
|
41
|
+
3. **Hit the crash** — The off-by-one `<=` causes a `TypeError` on `items[2]` when `items.length` is 2. Agent inspects the error, sees `i` equals `items.length`, identifies the off-by-one
|
|
42
|
+
|
|
43
|
+
4. **Hot-patch Bug 1** — Agent uses `agent-dbg hotpatch` to fix `i <= items.length` to `i < items.length` **in the running process** (no restart!)
|
|
44
|
+
|
|
45
|
+
5. **Continue and inspect** — Agent restarts the frame or continues. Orders process without crashing, but the discount amounts look wrong
|
|
46
|
+
|
|
47
|
+
6. **Investigate Bug 2** — Agent sets a breakpoint in `applyDiscount`, inspects `subtotal`, `rate`, and the return value. Evals `subtotal * rate` vs `subtotal * (1 - rate)` to confirm the formula is wrong
|
|
48
|
+
|
|
49
|
+
7. **Hot-patch Bug 2** — Agent patches `return subtotal * rate` to `return subtotal * (1 - rate)`
|
|
50
|
+
|
|
51
|
+
8. **Verify** — Agent continues execution, sees the correct totals in the output
|
|
52
|
+
|
|
53
|
+
### Talking Points
|
|
54
|
+
|
|
55
|
+
- **No restart needed**: Both fixes were applied to the live process via hot-patching
|
|
56
|
+
- **Agent reasoning**: The agent didn't just fix a crash — it reasoned about the discount math to catch the subtle logic bug
|
|
57
|
+
- **Token-efficient output**: agent-dbg's compact output format keeps the agent's context window lean
|
|
58
|
+
- **@ref system**: Variables use short references (@v1, @v2...) that the agent can use in subsequent commands without copying long object IDs
|
|
59
|
+
|
|
60
|
+
## Manual Verification
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Should crash (Bug 1)
|
|
64
|
+
node demo/order-processor.js
|
|
65
|
+
|
|
66
|
+
# After fixing Bug 1 (change <= to <), should show wrong discounts (Bug 2)
|
|
67
|
+
# ORD-001 total will be 25 instead of 100
|
|
68
|
+
|
|
69
|
+
# After fixing both bugs, correct output
|
|
70
|
+
node demo/order-processor.js
|
|
71
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Order Processor — calculates totals and applies discount codes
|
|
2
|
+
const orders = [
|
|
3
|
+
{ id: "ORD-001", items: [{ name: "Widget", price: 25, qty: 3 }, { name: "Gadget", price: 50, qty: 1 }], discount: "SAVE20" },
|
|
4
|
+
{ id: "ORD-002", items: [{ name: "Gizmo", price: 15, qty: 5 }], discount: null },
|
|
5
|
+
{ id: "ORD-003", items: [{ name: "Doohickey", price: 100, qty: 2 }, { name: "Widget", price: 25, qty: 1 }], discount: "SAVE20" },
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
const discountCodes = { SAVE20: 0.20, HALF: 0.50, VIP: 0.30 };
|
|
9
|
+
|
|
10
|
+
function calculateSubtotal(items) {
|
|
11
|
+
let subtotal = 0;
|
|
12
|
+
for (let i = 0; i < items.length; i++) {
|
|
13
|
+
subtotal += items[i].price * items[i].qty;
|
|
14
|
+
}
|
|
15
|
+
return subtotal;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function applyDiscount(subtotal, code) {
|
|
19
|
+
if (!code) return subtotal;
|
|
20
|
+
const rate = discountCodes[code];
|
|
21
|
+
return subtotal * (1 - rate);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function processOrders(orders) {
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const order of orders) {
|
|
27
|
+
const subtotal = calculateSubtotal(order.items);
|
|
28
|
+
const total = applyDiscount(subtotal, order.discount);
|
|
29
|
+
results.push({ id: order.id, subtotal, total });
|
|
30
|
+
}
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const results = processOrders(orders);
|
|
35
|
+
console.log("Order Results:", JSON.stringify(results, null, 2));
|