AI-Assisted Debuggingon the JVM

run(application)Unexpected behaviorWhy?

What to expect

Experience & intuition

Debugging approaches

What not to expect

A model leaderboard

One universal recipe

Beyond the JVM

My path to debugging

Algorithms

Kotlin Coroutines
IDE tooling

PLDI · PPoPP

Concurrency
Testing

Lincheck GitHub

Concurrent JVM code

CAV · ISSTA

Debugging

Execution traces
Failing schedules

AI-Assisted Debugging
Debugging production

I start with println()

OrderDemo.javaPricingService.java
3PricingService pricing = new PricingService(args.length == 0);
4Order order = new Order();
5Money result = pricing.price(order);
6if (!order.currency.equals(result.currency)) {
7 throw new AssertionError("expected " + order.currency + ", got " + result.currency);
8}
Console
AssertionError: expected EUR, got USD
  at OrderDemo.main(OrderDemo.java:7)
OrderDemo.javaPricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
4 if (cached != null) {
5 return useCached(cached);
6 } else {
7 return fetchPrice(order);
8 }
9}
Console
AssertionError: expected EUR, got USD

Which branch?

OrderDemo.javaPricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
4 if (cached != null) {
+ println("useCached");
6 return useCached(cached);
7 } else {
+ println("fetchPrice");
9 return fetchPrice(order);
10 }
11}
Console
useCached
AssertionError: expected EUR, got USD

Which call?

OrderDemo.javaPricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
+ println("sku=" + order.sku +
+ ", requested=" + order.currency +
+ ", cached=" + cached);
7 if (cached != null) {
+ println("useCached");
9 return useCached(cached);
10 } else {
+ println("fetchPrice");
12 return fetchPrice(order);
13 }
14}
Console
sku=JVM-1, requested=EUR, cached=90 USD
useCached
AssertionError: expected EUR, got USD

Which values?

Agents add logs too

CURSOR · 10 DECEMBER 2025

Introducing Debug Mode

Cursor Debug Mode article preview

ReLog

Generate diagnostic logs
Refine them after execution

Debugging framework · ASE 2026

A developer and an AI robot point at each other, each holding the same log printout
MeAI

An agent runs the same loop

PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
4 if (cached != null) {
5 return useCached(cached);
6 } else {
7 return fetchPrice(order);
8 }
9}
Console
AssertionError: expected EUR, got USD
AI ChatDemo
You · OrderDemo.java:7

Test failed: expected EUR, got USD

Locating the price calculation
Read PricingService.java
PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
4 if (cached != null) {
+ println("useCached " + cached);
6 return useCached(cached);
7 } else {
+ println("fetchPrice " + order);
9 return fetchPrice(order);
10 }
11}
Console
useCached 90 USD
AssertionError: expected EUR, got USD
AI ChatDemo
You · OrderDemo.java:7

Test failed: expected EUR, got USD

Checking both price sources
Add call + result logs
PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
4 if (cached != null) {
+ println("useCached " + cached);
6 return useCached(cached);
7 } else {
+ println("fetchPrice " + order);
9 return fetchPrice(order);
10 }
11}
Console
useCached 90 USD
AssertionError: expected EUR, got USD
AI ChatDemo
You · OrderDemo.java:7

Test failed: expected EUR, got USD

Checking which currency was requested
Inspect the request and cache key
PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
+ println("sku=" + order.sku +
+ ", requested=" + order.currency +
+ ", cached=" + cached);
7 if (cached != null) {
+ println("useCached " + cached);
9 return useCached(cached);
10 } else {
+ println("fetchPrice " + order);
12 return fetchPrice(order);
13 }
14}
Console
sku=JVM-1, requested=EUR, cached=90 USD
useCached 90 USD
AssertionError: expected EUR, got USD
AI ChatDemo
You · OrderDemo.java:7

Test failed: expected EUR, got USD

Comparing request and cached price
Log SKU, currency and cached value
PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
4 println("sku=" + order.sku +
5 ", requested=" + order.currency +
6 ", cached=" + cached);
7 if (cached != null) {
8 println("useCached " + cached);
9 return useCached(cached);
10 } else {
11 println("fetchPrice " + order);
12 return fetchPrice(order);
13 }
14}
Console
sku=JVM-1, requested=EUR, cached=90 USD
useCached 90 USD
AssertionError: expected EUR, got USD
AI ChatDemo
You · OrderDemo.java:7

Test failed: expected EUR, got USD

Connecting the observations
Read request and cached currency

Currency is missing from the cache key

A EUR order gets a USD price

0:00 / 0:19

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

A skeleton still waiting for its laptop to finish compiling

println() has limits

Change a log

Recompile + rerun

Inside a dependency

No source edits

A frustrated man pleading to get through a locked fence

Let’s use a debugger
for debugging

Run, pause, inspect, resume

PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
4 if (cached != null) {
5 return useCached(cached);
6 } else {
7 return fetchPrice(order);
8 }
9}
Debug: OrderDemoDebug
Debug session not started
Console
PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
4 if (cached != null) {
5 return useCached(cached);
6 } else {
7 return fetchPrice(order);
8 }
9}
Debug: OrderDemoRunning
Waiting for a breakpoint
Console
PricingService.java
1class PricingService {
2Money price(Order order) {order: Order@402
3 Money cached = prices.get(order.sku);90 USD
4 if (cached != null) {cached ≠ null
5 return useCached(cached);cached: 90 USD
6 } else {
7 return fetchPrice(order);
8 }
9}
Debug: OrderDemoResumeSuspended · main
Frames
main
price:5PricingService
main:5OrderDemo
Variables
this = {PricingService@401}
order = {Order@402}
id = 42
sku = "JVM-1"
currency = "EUR"
cached = {Money@403}"90 USD"
amount = 90
currency = "USD"
Console
PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
4 if (cached != null) {
5 return useCached(cached);
6 } else {
7 return fetchPrice(order);
8 }
9}
Debug: OrderDemoRerunFinished · exit code 1

Console

Exception in thread "main" java.lang.AssertionError:
  expected EUR, got USD
  at OrderDemo.main(OrderDemo.java:7)
Tests: 1 failed

Connect your agent to IntelliJ

Settings → Tools → MCP Server
Enable MCP Server
Your AI agentAuto-Configure

Other MCP clients → Copy Config

Restart the agent

Debugger tools are available

xdebug_set_breakpointxdebug_control_sessionxdebug_get_frame_values

MCP Server setup · Debugger tools · IDEA 2026.1.3+

An agent uses the debugger

PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
4 if (cached != null) {
5 return useCached(cached);
6 } else {
7 return fetchPrice(order);
8 }
9}
Debug: OrderDemoDebug
Debug session not started
Console
AI ChatDemo
You · OrderDemo.java:7

Test failed: expected EUR, got USD

Choosing where to pause
Set breakpoints · lines 5 and 7
PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
4 if (cached != null) {
5 return useCached(cached);
6 } else {
7 return fetchPrice(order);
8 }
9}
Debug: OrderDemoRunning
Waiting for a breakpoint
Console
AI ChatDemo
You · OrderDemo.java:7

Test failed: expected EUR, got USD

Waiting for either branch
Start OrderDemo
PricingService.java
1class PricingService {
2Money price(Order order) {order: Order@402
3 Money cached = prices.get(order.sku);90 USD
4 if (cached != null) {cached ≠ null
5 return useCached(cached);cached: 90 USD
6 } else {
7 return fetchPrice(order);
8 }
9}
Debug: OrderDemoResumeSuspended · main
Frames
main
price:5PricingService
main:5OrderDemo
Variables
this = {PricingService@401}
order = {Order@402}
id = 42
sku = "JVM-1"
currency = "EUR"
cached = {Money@403}"90 USD"
amount = 90
currency = "USD"
Console
AI ChatDemo
You · OrderDemo.java:7

Test failed: expected EUR, got USD

Inspecting the cached-price branch
Read order and cached
PricingService.java
1class PricingService {
2Money price(Order order) {order: Order@402
3 Money cached = prices.get(order.sku);90 USD
4 if (cached != null) {cached ≠ null
5 return useCached(cached);cached: 90 USD
6 } else {
7 return fetchPrice(order);
8 }
9}
Debug: OrderDemoResumeSuspended · main
Frames
main
price:5PricingService
main:5OrderDemo
Variables
this = {PricingService@401}
order = {Order@402}
id = 42
sku = "JVM-1"
currency = "EUR"
cached = {Money@403}"90 USD"
amount = 90
currency = "USD"
Console
AI ChatDemo
You · OrderDemo.java:7

Test failed: expected EUR, got USD

Checking the cache key
Read prices.get(order.sku)
PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
4 if (cached != null) {
5 return useCached(cached);
6 } else {
7 return fetchPrice(order);
8 }
9}
Debug: OrderDemoRerunFinished · exit code 1

Console

Exception in thread "main" java.lang.AssertionError:
  expected EUR, got USD
  at OrderDemo.main(OrderDemo.java:7)
Tests: 1 failed
AI ChatDemo
You · OrderDemo.java:7

Test failed: expected EUR, got USD

Connecting the observations
Resume the test

Currency is missing from the cache key

A EUR order gets a USD price

0:00 / 0:19

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

Edit + run
37.2%
+ interactive debugger
48.4%

The workflow matters too

Debug2Fix · Garg & Huang, Microsoft · February 2026

186 GitBug-Java tasks · pass rate

GPT-5Haiku 4.5Sonnet 4.5
Baseline60.2%71.0%75.7%
+ debugger tools60.8%70.4%64.5%
+ debugging subagent64.0%76.1%78.0%
Mandatory first debugger call73.1%82.3%85.5%
Edits lockedCall debug subagentEdits enabled

Logpoint: Debugger in println style

PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
4 if (cached != null) {
5 return useCached(cached);
6 } else {
7 return fetchPrice(order);
8 }
9}
Logpoint PricingService.java:4
"sku=" + order.sku +
", requested=" + order.currency +
", cached=" + cached
More
Done
Console
PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
Logpoint"sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached
4 if (cached != null) {
Logpoint"useCached"
5 return useCached(cached);
6 } else {
Logpoint"fetchPrice"
7 return fetchPrice(order);
8 }
9}
Console
PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
Logpoint"sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached
4 if (cached != null) {
Logpoint"useCached"
5 return useCached(cached);
6 } else {
Logpoint"fetchPrice"
7 return fetchPrice(order);
8 }
9}
Console
sku=JVM-1, requested=EUR, cached=90 USD
useCached
AssertionError: expected EUR, got USD
Completed
without
suspension

Logpoints, ready for agents

JETBRAINS · INTELLIJ IDEA 2026.2

JetBrains Println Debugging Done Right article cover

Set observations

Run once

Read the output

No step / resume loop

The agent sets logpoints

PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
Logpoint"sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached
4 if (cached != null) {
Logpoint"useCached"
5 return useCached(cached);
6 } else {
Logpoint"fetchPrice"
7 return fetchPrice(order);
8 }
9}
Console
AI ChatDemo
You · OrderDemo.java:7

Test failed: expected EUR, got USD

Preparing the observations
Set all logpoints · lines 4, 5 and 7
PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
Logpoint"sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached
4 if (cached != null) {
Logpoint"useCached"
5 return useCached(cached);
6 } else {
Logpoint"fetchPrice"
7 return fetchPrice(order);
8 }
9}
Console
sku=JVM-1, requested=EUR, cached=90 USD
useCached
AssertionError: expected EUR, got USD
AI ChatDemo
You · OrderDemo.java:7

Test failed: expected EUR, got USD

Running without pauses
Completed without suspension
PricingService.java
1class PricingService {
2Money price(Order order) {
3 Money cached = prices.get(order.sku);
Logpoint"sku=" + order.sku + ", requested=" + order.currency + ", cached=" + cached
4 if (cached != null) {
Logpoint"useCached"
5 return useCached(cached);
6 } else {
Logpoint"fetchPrice"
7 return fetchPrice(order);
8 }
9}
Console
sku=JVM-1, requested=EUR, cached=90 USD
useCached
AssertionError: expected EUR, got USD
AI ChatDemo
You · OrderDemo.java:7

Test failed: expected EUR, got USD

Connecting the observations
Read values and executed branch

Currency is missing from the cache key

A EUR order gets a USD price

0:00 / 0:11

Logpoints or breakpoints?

Know what to observe?

Logpoints

Set → Run → Read

Need to explore?

Breakpoints

Pause → Inspect → Choose next

Logpoints first · pause when needed

The bundled ij-debugger skill follows this strategy

Debugging
without a debugger?

A confused suited man looking around for the missing debugger

Two invoices, one number

Webshop example · Igor Kulakov, JetBrains · May 2026

Request A

INV-00001

Request B

INV-00002

Passing run · sequential requests

Request A

INV-00001

Request B

INV-00001

Rare failure · overlapping requests

A shocked hamster reacting to the duplicate invoice number

Same lines, different hit counts

IntelliJ coverage · two controlled JVM runs

InvoiceService.javaPassFail
19if (generator == null) {
20 generator = createGenerator();
22number = generator.nextNumber();
28max = repository.findMaxInvoiceNumber();

100% line coverage in both runs

InvoiceService.javaPassFail
19if (generator == null) {22
20 generator = createGenerator();12
22number = generator.nextNumber();22
28max = repository.findMaxInvoiceNumber();12

Inspect generator initialization

Sampling gives us a direction

Sampling profiler

ILLUSTRATIVE STACK SAMPLES
AppDBAppDBDBAppAppDBApp
InvoiceService.createGenerator()

A clue where to look

Rare calls may be missed

An option in production

Every execution has a history

Trace Recorder Order 42

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

1 secondof execution
1 GBof full trace

Calls · arguments · locals · reads · writes

Order of magnitude · varies with the program

What deserves the detail?

A frantic investigator connecting far too many log printouts on a conspiracy board

Keep the useful parts

First 2 + last 2 · preserve selected failures

0199
Iteration 0USDUSD
Iteration 1USDUSD
196 more iterations
Iteration 198USDUSD
Iteration 199EURUSD

First 2 + last 2 · preserve selected failures

0199
Iteration 0USDUSD
Iteration 1USDUSD
196 iterations omitted
Iteration 198USDUSD
Iteration 199EURUSD
Illustrative trace 200 iterations · 1,409 events
Batch@401.run()
loadCatalog()
decode("90 USD") → Money@403
String.split(" ") → ["90", "USD"]
Integer.parseInt("90") → 90
normalize(Money@403) → Money@403
index(Money@403)
prices.put("JVM-1", Money@403) → null
for each order
iteration 0
price(Order@404) → Money@403
prices.get("JVM-1") → Money@403
useCached(Money@403) → Money@403
validateCurrency(Order@404, Money@403)
expected = "USD", actual = "USD"
passed = 1
… iterations 1198
iteration 199
price(Order@603) → Money@403
prices.get("JVM-1") → Money@403
useCached(Money@403) → Money@403
validateCurrency(Order@603, Money@403)
expected = "EUR", actual = "USD"
throw AssertionError
Selected view 1 event · Failure
throw AssertionError

Start with failures or logs

Throw · catch · assertion

Logging · stdout · stderr

Illustrative trace 200 iterations · 1,409 events
Batch@401.run()
loadCatalog()
decode("90 USD") → Money@403
String.split(" ") → ["90", "USD"]
Integer.parseInt("90") → 90
normalize(Money@403) → Money@403
index(Money@403)
prices.put("JVM-1", Money@403) → null
for each order
iteration 0
price(Order@404) → Money@403
prices.get("JVM-1") → Money@403
useCached(Money@403) → Money@403
validateCurrency(Order@404, Money@403)
expected = "USD", actual = "USD"
passed = 1
… iterations 1198
iteration 199
price(Order@603) → Money@403
prices.get("JVM-1") → Money@403
useCached(Money@403) → Money@403
validateCurrency(Order@603, Money@403)
expected = "EUR", actual = "USD"
throw AssertionError
Selected view 5 events · + call path
Batch@401.run()
for each order
iteration 199
validateCurrency(Order@603, Money@403)
throw AssertionError
Illustrative trace 200 iterations · 1,409 events
Batch@401.run()
loadCatalog()
decode("90 USD") → Money@403
String.split(" ") → ["90", "USD"]
Integer.parseInt("90") → 90
normalize(Money@403) → Money@403
index(Money@403)
prices.put("JVM-1", Money@403) → null
for each order
iteration 0
price(Order@404) → Money@403
prices.get("JVM-1") → Money@403
useCached(Money@403) → Money@403
validateCurrency(Order@404, Money@403)
expected = "USD", actual = "USD"
passed = 1
… iterations 1198
iteration 199
price(Order@603) → Money@403
prices.get("JVM-1") → Money@403
useCached(Money@403) → Money@403
validateCurrency(Order@603, Money@403)
expected = "EUR", actual = "USD"
throw AssertionError
Selected view 7 events · + nearby state
Batch@401.run()
for each order
iteration 199
price(Order@603) → Money@403
validateCurrency(Order@603, Money@403)
expected = "EUR", actual = "USD"
throw AssertionError

Keep the useful parts

Measured example · Kotlin compiler · KT-75831

16.4Mrecorded events
756selected events
128 kB of text for the agent

Trace Recorder: lower cost

Our internal experiments · combined results

AVERAGE MODEL COST · REPORTED UNITS

Without trace
7.806
Trace Recorder
3.336
57%lower cost
SOLVED46.7% → 50.9%

Uncertain gain
p = 0.15 · interval −1.5 to +9.9 pp

AVERAGE TIME541 → 588 s

Lower cost
Longer runs

Would you do any of this
in production?

A calm cartoon dog with coffee and a laptop surrounded by flames

Production changes the rules

Extra printlnRebuild + redeploy
Remote debuggerPauses · mutable state · JDWP
IDE logpointsDebug connection · mutable state
Coverage comparisonNo controlled reruns
Execution tracesToo much to record
Sampling profilerCan run continuously · partial evidence
×Extra printlnRebuild + redeploy
Remote debuggerPauses · mutable state · JDWP
IDE logpointsDebug connection · mutable state
Coverage comparisonNo controlled reruns
Execution tracesToo much to record
Sampling profilerCan run continuously · partial evidence
×Extra printlnRebuild + redeploy
×Remote debuggerPauses · mutable state · JDWP
IDE logpointsDebug connection · mutable state
Coverage comparisonNo controlled reruns
Execution tracesToo much to record
Sampling profilerCan run continuously · partial evidence
×Extra printlnRebuild + redeploy
×Remote debuggerPauses · mutable state · JDWP
×IDE logpointsDebug connection · mutable state
Coverage comparisonNo controlled reruns
Execution tracesToo much to record
Sampling profilerCan run continuously · partial evidence
×Extra printlnRebuild + redeploy
×Remote debuggerPauses · mutable state · JDWP
×IDE logpointsDebug connection · mutable state
×Coverage comparisonNo controlled reruns
Execution tracesToo much to record
Sampling profilerCan run continuously · partial evidence
×Extra printlnRebuild + redeploy
×Remote debuggerPauses · mutable state · JDWP
×IDE logpointsDebug connection · mutable state
×Coverage comparisonNo controlled reruns
×Execution tracesToo much to record
Sampling profilerCan run continuously · partial evidence
×Extra printlnRebuild + redeploy
×Remote debuggerPauses · mutable state · JDWP
×IDE logpointsDebug connection · mutable state
×Coverage comparisonNo controlled reruns
×Execution tracesToo much to record
Sampling profilerCan run continuously · partial evidence

Keep the values
Keep the app running

Live requestResponse

Snapshot

order.currency = "EUR"
cached.currency = "USD"

Verify the root cause
of production incidents

appglass.org

How AppGlass makes it possible

AppGlass Java agent installed in the JVM

Production restrictionAppGlass mechanism
Rebuild + redeployInstall tracepoints at runtime
Production restrictionAppGlass mechanism
Rebuild + redeployInstall tracepoints at runtime
Thread pausesCapture values, then continue
Production restrictionAppGlass mechanism
Rebuild + redeployInstall tracepoints at runtime
Thread pausesCapture values, then continue
Mutable stateValidated read-only expressions
Production restrictionAppGlass mechanism
Rebuild + redeployInstall tracepoints at runtime
Thread pausesCapture values, then continue
Mutable stateValidated read-only expressions
JDWP connectionConnect through the AppGlass Java agent
Production restrictionAppGlass mechanism
Rebuild + redeployInstall tracepoints at runtime
Thread pausesCapture values, then continue
Mutable stateValidated read-only expressions
JDWP connectionConnect through the AppGlass Java agent
No controlled rerunsCapture the next matching live request
Production restrictionAppGlass mechanism
Rebuild + redeployInstall tracepoints at runtime
Thread pausesCapture values, then continue
Mutable stateValidated read-only expressions
JDWP connectionConnect through the AppGlass Java agent
No controlled rerunsCapture the next matching live request
Too much to recordSelected snapshots + capture limits

Expression safety checker

order.id == 42

Expression

Compile

Application types

Check

Bytecode + callees

Install

Validated only

MutationI/OLocksLong loopsDeep recursionBlocked

Unsafe at runtime → tracepoint disabled

Gandalf blocks the way for unsafe expressions

Control plane

AI agent

AI agent

Code editor

IntelliJ IDEA

Control plane routes connections

Control plane

Application JVM

Java agent inside the application JVM

Java agent

No inbound port needed on the application

Runtime evidence for debugging

ApproachRuntime evidenceProduction
printlnChosen events + valuesCode change + redeploy
DebuggerFrames + object statePauses · can change state
IDE logpointsChosen events + valuesDebug connection required
CoverageLines + execution countsComparable runs needed
ProfilerSampled call stacksAvailable in production
Execution traceCalls + state over timeFull capture is too large
AppGlassSelected runtime snapshotsRead-only, without pauses

Give the AI agent runtime evidence