AI-Assisted Debuggingon the JVM
What to expect
Experience & intuition
Debugging approaches
What not to expect
A model leaderboard
One universal recipe
Beyond the JVM
My path to debugging
Debugging
Execution traces
Failing schedules
AI-Assisted Debugging
Debugging production
I start with println()
PricingService pricing = new PricingService(args.length == 0);Order order = new Order();Money result = pricing.price(order);if (!order.currency.equals(result.currency)) { throw new AssertionError("expected " + order.currency + ", got " + result.currency);}AssertionError: expected EUR, got USD at OrderDemo.main(OrderDemo.java:7)
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); if (cached != null) { return useCached(cached); } else { return fetchPrice(order); }}AssertionError: expected EUR, got USDWhich branch?
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); if (cached != null) { println("useCached"); return useCached(cached); } else { println("fetchPrice"); return fetchPrice(order); }}useCached
AssertionError: expected EUR, got USDWhich call?
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); println("sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached); if (cached != null) { println("useCached"); return useCached(cached); } else { println("fetchPrice"); return fetchPrice(order); }}sku=JVM-1, requested=EUR, cached=90 USD useCached AssertionError: expected EUR, got USD
Which values?
Agents add logs too
5/37An agent runs the same loop
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); if (cached != null) { return useCached(cached); } else { return fetchPrice(order); }}AssertionError: expected EUR, got USDTest failed: expected EUR, got USD
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); if (cached != null) { println("useCached " + cached); return useCached(cached); } else { println("fetchPrice " + order); return fetchPrice(order); }}useCached 90 USD AssertionError: expected EUR, got USD
Test failed: expected EUR, got USD
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); if (cached != null) { println("useCached " + cached); return useCached(cached); } else { println("fetchPrice " + order); return fetchPrice(order); }}useCached 90 USD AssertionError: expected EUR, got USD
Test failed: expected EUR, got USD
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); println("sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached); if (cached != null) { println("useCached " + cached); return useCached(cached); } else { println("fetchPrice " + order); return fetchPrice(order); }}sku=JVM-1, requested=EUR, cached=90 USD useCached 90 USD AssertionError: expected EUR, got USD
Test failed: expected EUR, got USD
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); println("sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached); if (cached != null) { println("useCached " + cached); return useCached(cached); } else { println("fetchPrice " + order); return fetchPrice(order); }}sku=JVM-1, requested=EUR, cached=90 USD useCached 90 USD AssertionError: expected EUR, got USD
Test failed: expected EUR, got USD
Currency is missing from the cache key
A EUR order gets a USD price
Logs need a feedback loop
ReLog · ASE 2026 · revised 26 August 2026
DeepSeek-V3 · 311 faulty Java methods · correct repairs
One round of logging
Add logs → Run → Diagnose
26.0%Improve the observations
Add logs → Run → Read output
↳ Revise logs → Rerun → Diagnose
31.2%println() has limits
Change a log
Recompile + rerun
Inside a dependency
No source edits
println() has limits
Change a log
Recompile + rerun
Inside a dependency
No source edits
Let’s use a debugger
for debugging
Run, pause, inspect, resume
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); if (cached != null) { return useCached(cached); } else { return fetchPrice(order); }}class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); if (cached != null) { return useCached(cached); } else { return fetchPrice(order); }}class PricingService {Money price(Order order) {order: Order@402 Money cached = prices.get(order.sku);90 USD if (cached != null) {cached ≠ null return useCached(cached);cached: 90 USD } else { return fetchPrice(order); }}class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); if (cached != null) { return useCached(cached); } else { return fetchPrice(order); }}Console
Exception in thread "main" java.lang.AssertionError: expected EUR, got USD at OrderDemo.main(OrderDemo.java:7)
Connect your agent to IntelliJ
Other MCP clients → Copy Config
Restart the agent
Debugger tools are available
xdebug_set_breakpointxdebug_control_sessionxdebug_get_frame_valuesAn agent uses the debugger
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); if (cached != null) { return useCached(cached); } else { return fetchPrice(order); }}Test failed: expected EUR, got USD
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); if (cached != null) { return useCached(cached); } else { return fetchPrice(order); }}Test failed: expected EUR, got USD
class PricingService {Money price(Order order) {order: Order@402 Money cached = prices.get(order.sku);90 USD if (cached != null) {cached ≠ null return useCached(cached);cached: 90 USD } else { return fetchPrice(order); }}Test failed: expected EUR, got USD
class PricingService {Money price(Order order) {order: Order@402 Money cached = prices.get(order.sku);90 USD if (cached != null) {cached ≠ null return useCached(cached);cached: 90 USD } else { return fetchPrice(order); }}Test failed: expected EUR, got USD
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); if (cached != null) { return useCached(cached); } else { return fetchPrice(order); }}Console
Exception in thread "main" java.lang.AssertionError: expected EUR, got USD at OrderDemo.main(OrderDemo.java:7)
Test failed: expected EUR, got USD
Currency is missing from the cache key
A EUR order gets a USD price
Does debugger access help?
debug-gym · Microsoft Research · March 2025
Claude 3.7 Sonnet · 300 SWE-bench Lite Python tasks
TASKS SOLVED · MEAN OF 3 RUNS
The workflow matters too
Debug2Fix · Garg & Huang, Microsoft · February 2026
186 GitBug-Java tasks · pass rate
| GPT-5 | Haiku 4.5 | Sonnet 4.5 | |
|---|---|---|---|
| Baseline | 60.2% | 71.0% | 75.7% |
| + debugger tools | 60.8% | 70.4% | 64.5% |
| + debugging subagent | 64.0% | 76.1% | 78.0% |
| Mandatory first debugger call | 73.1% | 82.3% | 85.5% |
Logpoint: Debugger in println style
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku); if (cached != null) { return useCached(cached); } else { return fetchPrice(order); }}"sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku);"sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached if (cached != null) {"useCached" return useCached(cached); } else {"fetchPrice" return fetchPrice(order); }}class PricingService {Money price(Order order) { Money cached = prices.get(order.sku);"sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached if (cached != null) {"useCached" return useCached(cached); } else {"fetchPrice" return fetchPrice(order); }}sku=JVM-1, requested=EUR, cached=90 USD useCached AssertionError: expected EUR, got USD
without
suspension
Logpoints, ready for agents
JETBRAINS · INTELLIJ IDEA 2026.2
Set observations
Run once
Read the output
No step / resume loop
The agent sets logpoints
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku);"sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached if (cached != null) {"useCached" return useCached(cached); } else {"fetchPrice" return fetchPrice(order); }}Test failed: expected EUR, got USD
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku);"sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached if (cached != null) {"useCached" return useCached(cached); } else {"fetchPrice" return fetchPrice(order); }}sku=JVM-1, requested=EUR, cached=90 USD useCached AssertionError: expected EUR, got USD
Test failed: expected EUR, got USD
class PricingService {Money price(Order order) { Money cached = prices.get(order.sku);"sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached if (cached != null) {"useCached" return useCached(cached); } else {"fetchPrice" return fetchPrice(order); }}sku=JVM-1, requested=EUR, cached=90 USD useCached AssertionError: expected EUR, got USD
Test failed: expected EUR, got USD
Currency is missing from the cache key
A EUR order gets a USD price
Logpoints or breakpoints?
Know what to observe?
Logpoints
Set → Run → Read
Need to explore?
Breakpoints
Pause → Inspect → Choose next
Debugging
without a debugger?
Two invoices, one number
Webshop example · Igor Kulakov, JetBrains · May 2026
Request A
INV-00001Request B
INV-00002Passing run · sequential requests
Request A
INV-00001Request B
INV-00001Rare failure · overlapping requests
Same lines, different hit counts
IntelliJ coverage · two controlled JVM runs
| InvoiceService.java | Pass | Fail |
|---|---|---|
19if (generator == null) { | ✓ | ✓ |
20 generator = createGenerator(); | ✓ | ✓ |
22number = generator.nextNumber(); | ✓ | ✓ |
28max = repository.findMaxInvoiceNumber(); | ✓ | ✓ |
100% line coverage in both runs
| InvoiceService.java | Pass | Fail |
|---|---|---|
19if (generator == null) { | 2 | 2 |
20 generator = createGenerator(); | 1 | 2 |
22number = generator.nextNumber(); | 2 | 2 |
28max = repository.findMaxInvoiceNumber(); | 1 | 2 |
Inspect generator initialization
Sampling gives us a direction
InvoiceService.createGenerator()A clue where to look
Rare calls may be missed
An option in production
Every execution has a history
price(order) EUR
prices.get("JVM-1") → 90 USD
cached != null → true
useCached(cached) → 90 USD
AssertionError: expected EUR, got USD
Every event is too much
Calls · arguments · locals · reads · writes
Order of magnitude · varies with the program
What deserves the detail?
Keep the useful parts
First 2 + last 2 · preserve selected failures
USD → USDUSD → USDUSD → USDEUR → USDFirst 2 + last 2 · preserve selected failures
USD → USDUSD → USDUSD → USDEUR → USDBatch@401.run()loadCatalog()decode("90 USD") → Money@403String.split(" ") → ["90", "USD"]Integer.parseInt("90") → 90normalize(Money@403) → Money@403index(Money@403)prices.put("JVM-1", Money@403) → nullfor each orderiteration 0price(Order@404) → Money@403prices.get("JVM-1") → Money@403useCached(Money@403) → Money@403validateCurrency(Order@404, Money@403)expected = "USD", actual = "USD"passed = 1… iterations 1–198 …iteration 199price(Order@603) → Money@403prices.get("JVM-1") → Money@403useCached(Money@403) → Money@403validateCurrency(Order@603, Money@403)expected = "EUR", actual = "USD"throw AssertionErrorthrow AssertionErrorStart with failures or logs
Throw · catch · assertion
Logging · stdout · stderr
Batch@401.run()loadCatalog()decode("90 USD") → Money@403String.split(" ") → ["90", "USD"]Integer.parseInt("90") → 90normalize(Money@403) → Money@403index(Money@403)prices.put("JVM-1", Money@403) → nullfor each orderiteration 0price(Order@404) → Money@403prices.get("JVM-1") → Money@403useCached(Money@403) → Money@403validateCurrency(Order@404, Money@403)expected = "USD", actual = "USD"passed = 1… iterations 1–198 …iteration 199price(Order@603) → Money@403prices.get("JVM-1") → Money@403useCached(Money@403) → Money@403validateCurrency(Order@603, Money@403)expected = "EUR", actual = "USD"throw AssertionErrorBatch@401.run()for each orderiteration 199validateCurrency(Order@603, Money@403)throw AssertionErrorBatch@401.run()loadCatalog()decode("90 USD") → Money@403String.split(" ") → ["90", "USD"]Integer.parseInt("90") → 90normalize(Money@403) → Money@403index(Money@403)prices.put("JVM-1", Money@403) → nullfor each orderiteration 0price(Order@404) → Money@403prices.get("JVM-1") → Money@403useCached(Money@403) → Money@403validateCurrency(Order@404, Money@403)expected = "USD", actual = "USD"passed = 1… iterations 1–198 …iteration 199price(Order@603) → Money@403prices.get("JVM-1") → Money@403useCached(Money@403) → Money@403validateCurrency(Order@603, Money@403)expected = "EUR", actual = "USD"throw AssertionErrorBatch@401.run()for each orderiteration 199price(Order@603) → Money@403validateCurrency(Order@603, Money@403)expected = "EUR", actual = "USD"throw AssertionErrorKeep the useful parts
Measured example · Kotlin compiler · KT-75831
Trace Recorder: lower cost
Our internal experiments · combined results
AVERAGE MODEL COST · REPORTED UNITS
Uncertain gain
p = 0.15 · interval −1.5 to +9.9 pp
Lower cost
Longer runs
Would you do any of this
in production?
Production changes the rules
| Extra println | Rebuild + redeploy |
| Remote debugger | Pauses · mutable state · JDWP |
| IDE logpoints | Debug connection · mutable state |
| Coverage comparison | No controlled reruns |
| Execution traces | Too much to record |
| Sampling profiler | Can run continuously · partial evidence |
| ×Extra println | Rebuild + redeploy |
| Remote debugger | Pauses · mutable state · JDWP |
| IDE logpoints | Debug connection · mutable state |
| Coverage comparison | No controlled reruns |
| Execution traces | Too much to record |
| Sampling profiler | Can run continuously · partial evidence |
| ×Extra println | Rebuild + redeploy |
| ×Remote debugger | Pauses · mutable state · JDWP |
| IDE logpoints | Debug connection · mutable state |
| Coverage comparison | No controlled reruns |
| Execution traces | Too much to record |
| Sampling profiler | Can run continuously · partial evidence |
| ×Extra println | Rebuild + redeploy |
| ×Remote debugger | Pauses · mutable state · JDWP |
| ×IDE logpoints | Debug connection · mutable state |
| Coverage comparison | No controlled reruns |
| Execution traces | Too much to record |
| Sampling profiler | Can run continuously · partial evidence |
| ×Extra println | Rebuild + redeploy |
| ×Remote debugger | Pauses · mutable state · JDWP |
| ×IDE logpoints | Debug connection · mutable state |
| ×Coverage comparison | No controlled reruns |
| Execution traces | Too much to record |
| Sampling profiler | Can run continuously · partial evidence |
| ×Extra println | Rebuild + redeploy |
| ×Remote debugger | Pauses · mutable state · JDWP |
| ×IDE logpoints | Debug connection · mutable state |
| ×Coverage comparison | No controlled reruns |
| ×Execution traces | Too much to record |
| Sampling profiler | Can run continuously · partial evidence |
| ×Extra println | Rebuild + redeploy |
| ×Remote debugger | Pauses · mutable state · JDWP |
| ×IDE logpoints | Debug connection · mutable state |
| ×Coverage comparison | No controlled reruns |
| ×Execution traces | Too much to record |
| ✓Sampling profiler | Can run continuously · partial evidence |
Keep the values
Keep the app running
Snapshot
order.currency = "EUR"
cached.currency = "USD"How AppGlass makes it possible
AppGlass Java agent installed in the JVM
Expression safety checker
order.id == 42Expression
Compile
Application types
Check
Bytecode + callees
Install
Validated only
Unsafe at runtime → tracepoint disabled
Control plane
AI agent
IntelliJ IDEA
Control plane
Application JVM
Java agent
No inbound port needed on the application
Runtime evidence for debugging
| Approach | Runtime evidence | Production |
|---|---|---|
| println | Chosen events + values | Code change + redeploy |
| Debugger | Frames + object state | Pauses · can change state |
| IDE logpoints | Chosen events + values | Debug connection required |
| Coverage | Lines + execution counts | Comparable runs needed |
| Profiler | Sampled call stacks | Available in production |
| Execution trace | Calls + state over time | Full capture is too large |
| AppGlass | Selected runtime snapshots | Read-only, without pauses |
Give the AI agent runtime evidence