girb 0.1.2 → 0.3.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -0
- data/README.md +283 -129
- data/README_ja.md +280 -126
- data/lib/girb/ai_client.rb +209 -29
- data/lib/girb/auto_continue.rb +33 -0
- data/lib/girb/conversation_history.rb +8 -7
- data/lib/girb/debug_context_builder.rb +113 -0
- data/lib/girb/debug_integration.rb +426 -0
- data/lib/girb/debug_prompt_builder.rb +241 -0
- data/lib/girb/debug_session_history.rb +121 -0
- data/lib/girb/exception_capture.rb +4 -0
- data/lib/girb/girbrc_loader.rb +15 -9
- data/lib/girb/irb_integration.rb +233 -1
- data/lib/girb/prompt_builder.rb +156 -46
- data/lib/girb/session_persistence.rb +170 -0
- data/lib/girb/tools/continue_analysis.rb +45 -0
- data/lib/girb/tools/debug_session_history_tool.rb +132 -0
- data/lib/girb/tools/evaluate_code.rb +24 -3
- data/lib/girb/tools/run_debug_command.rb +49 -0
- data/lib/girb/tools/run_irb_debug_command.rb +58 -0
- data/lib/girb/tools/session_history_tool.rb +46 -8
- data/lib/girb/tools.rb +23 -2
- data/lib/girb/version.rb +1 -1
- data/lib/girb.rb +24 -7
- data/lib/irb/command/qq.rb +48 -6
- metadata +11 -1
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "debug"
|
|
4
|
+
require_relative "debug_context_builder"
|
|
5
|
+
require_relative "debug_prompt_builder"
|
|
6
|
+
require_relative "debug_session_history"
|
|
7
|
+
require_relative "session_persistence"
|
|
8
|
+
|
|
9
|
+
module Girb
|
|
10
|
+
module DebugIntegration
|
|
11
|
+
# Define at module level so it's accessible as Girb::DebugIntegration::GIRB_DIR
|
|
12
|
+
# Points to lib directory, not gem root, so user's files aren't filtered
|
|
13
|
+
GIRB_DIR = File.expand_path('..', __dir__)
|
|
14
|
+
|
|
15
|
+
@ai_triggered = false
|
|
16
|
+
@interrupted = false
|
|
17
|
+
@session_started = false
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
attr_accessor :ai_triggered, :auto_continue, :interrupted, :api_thread
|
|
21
|
+
|
|
22
|
+
def pending_debug_commands
|
|
23
|
+
@pending_debug_commands ||= []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def add_pending_debug_command(cmd)
|
|
27
|
+
pending_debug_commands << cmd
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def take_pending_debug_commands
|
|
31
|
+
cmds = @pending_debug_commands || []
|
|
32
|
+
@pending_debug_commands = []
|
|
33
|
+
cmds
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def auto_continue?
|
|
37
|
+
@auto_continue
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def interrupted?
|
|
41
|
+
@interrupted
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def interrupt!
|
|
45
|
+
@interrupted = true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def clear_interrupt!
|
|
49
|
+
@interrupted = false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def session_started?
|
|
53
|
+
@session_started
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def start_session!
|
|
57
|
+
return if @session_started
|
|
58
|
+
|
|
59
|
+
SessionPersistence.start_session
|
|
60
|
+
@session_started = true
|
|
61
|
+
setup_exit_hook
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def save_session!
|
|
65
|
+
SessionPersistence.save_session if @session_started
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def setup
|
|
69
|
+
return unless defined?(DEBUGGER__::SESSION)
|
|
70
|
+
return if @setup_done
|
|
71
|
+
|
|
72
|
+
register_ai_command
|
|
73
|
+
register_debug_tools
|
|
74
|
+
setup_keybinding
|
|
75
|
+
patch_debugger_frame_filter
|
|
76
|
+
@setup_done = true
|
|
77
|
+
puts "[girb] Debug AI assistant loaded. Use 'qq <question>' or Ctrl+Space."
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def patch_debugger_frame_filter
|
|
81
|
+
return unless defined?(DEBUGGER__)
|
|
82
|
+
return if @frame_filter_patched
|
|
83
|
+
|
|
84
|
+
# girbのフレームもdebuggerのスタックトレースから除外
|
|
85
|
+
if DEBUGGER__.respond_to?(:capture_frames)
|
|
86
|
+
original_method = DEBUGGER__.method(:capture_frames)
|
|
87
|
+
DEBUGGER__.define_singleton_method(:capture_frames) do |*args|
|
|
88
|
+
frames = original_method.call(*args)
|
|
89
|
+
frames.reject! do |frame|
|
|
90
|
+
frame.realpath&.start_with?(Girb::DebugIntegration::GIRB_DIR)
|
|
91
|
+
end
|
|
92
|
+
frames
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@frame_filter_patched = true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# IRBからdebugモードに入った時に呼ばれる
|
|
100
|
+
def setup_if_needed
|
|
101
|
+
return if @setup_done
|
|
102
|
+
setup
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def register_ai_command
|
|
108
|
+
DEBUGGER__::SESSION.class.prepend(GirbDebugCommands)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def register_debug_tools
|
|
112
|
+
require_relative "tools/run_debug_command"
|
|
113
|
+
Girb::Tools.register(Girb::Tools::RunDebugCommand)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def setup_keybinding
|
|
117
|
+
return unless defined?(Reline::LineEditor)
|
|
118
|
+
|
|
119
|
+
Reline::LineEditor.prepend(Module.new do
|
|
120
|
+
private def girb_debug_ai_prefix(key)
|
|
121
|
+
Girb::DebugIntegration.ai_triggered = true
|
|
122
|
+
finish
|
|
123
|
+
end
|
|
124
|
+
end)
|
|
125
|
+
|
|
126
|
+
Reline.core.config.add_default_key_binding_by_keymap(:emacs, [0], :girb_debug_ai_prefix)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def setup_exit_hook
|
|
130
|
+
at_exit do
|
|
131
|
+
Girb::DebugIntegration.save_session!
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
module GirbDebugCommands
|
|
137
|
+
MAX_AUTO_CONTINUE = 20
|
|
138
|
+
|
|
139
|
+
def wait_command
|
|
140
|
+
# First, check for any pending commands (e.g., injected qq commands from IRB mode transition)
|
|
141
|
+
# Process these before entering auto_continue or waiting for user input
|
|
142
|
+
pending_cmds = Girb::DebugIntegration.take_pending_debug_commands
|
|
143
|
+
if pending_cmds.any?
|
|
144
|
+
pending_cmds.each do |cmd|
|
|
145
|
+
result = process_command(cmd)
|
|
146
|
+
return result unless result == :retry
|
|
147
|
+
end
|
|
148
|
+
return :retry
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if Girb::DebugIntegration.auto_continue?
|
|
152
|
+
@girb_auto_continue_count ||= 0
|
|
153
|
+
@girb_auto_continue_count += 1
|
|
154
|
+
|
|
155
|
+
# Set up interrupt handler on first iteration
|
|
156
|
+
if @girb_auto_continue_count == 1
|
|
157
|
+
setup_interrupt_handler
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Check for interrupt (Ctrl+C)
|
|
161
|
+
if Girb::DebugIntegration.interrupted?
|
|
162
|
+
Girb::DebugIntegration.auto_continue = false
|
|
163
|
+
Girb::DebugIntegration.clear_interrupt!
|
|
164
|
+
@girb_auto_continue_count = 0
|
|
165
|
+
restore_interrupt_handler
|
|
166
|
+
handle_ai_interrupted
|
|
167
|
+
return :retry
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if @girb_auto_continue_count > MAX_AUTO_CONTINUE
|
|
171
|
+
Girb::DebugIntegration.auto_continue = false
|
|
172
|
+
@girb_auto_continue_count = 0
|
|
173
|
+
restore_interrupt_handler
|
|
174
|
+
handle_ai_turn_limit_reached
|
|
175
|
+
return :retry
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
begin
|
|
179
|
+
handle_ai_continuation
|
|
180
|
+
rescue Exception => e
|
|
181
|
+
if e.is_a?(Interrupt) || e.class.name.include?("Interrupt") || e.class.name.include?("Abort")
|
|
182
|
+
Girb::DebugIntegration.auto_continue = false
|
|
183
|
+
Girb::DebugIntegration.clear_interrupt!
|
|
184
|
+
@girb_auto_continue_count = 0
|
|
185
|
+
restore_interrupt_handler
|
|
186
|
+
handle_ai_interrupted
|
|
187
|
+
return :retry
|
|
188
|
+
else
|
|
189
|
+
raise
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Check for interrupt after API call (Ctrl+C during request)
|
|
194
|
+
if Girb::DebugIntegration.interrupted?
|
|
195
|
+
Girb::DebugIntegration.auto_continue = false
|
|
196
|
+
Girb::DebugIntegration.clear_interrupt!
|
|
197
|
+
@girb_auto_continue_count = 0
|
|
198
|
+
restore_interrupt_handler
|
|
199
|
+
handle_ai_interrupted
|
|
200
|
+
return :retry
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
more_cmds = Girb::DebugIntegration.take_pending_debug_commands
|
|
204
|
+
if more_cmds.any?
|
|
205
|
+
more_cmds.each do |cmd|
|
|
206
|
+
result = process_command(cmd)
|
|
207
|
+
return result unless result == :retry
|
|
208
|
+
end
|
|
209
|
+
else
|
|
210
|
+
Girb::DebugIntegration.auto_continue = false
|
|
211
|
+
restore_interrupt_handler
|
|
212
|
+
end
|
|
213
|
+
return :retry
|
|
214
|
+
else
|
|
215
|
+
@girb_auto_continue_count = 0
|
|
216
|
+
Girb::DebugIntegration.clear_interrupt!
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
super
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def process_command(line)
|
|
223
|
+
if Girb::DebugIntegration.ai_triggered
|
|
224
|
+
Girb::DebugIntegration.ai_triggered = false
|
|
225
|
+
question = line.strip
|
|
226
|
+
return :retry if question.empty?
|
|
227
|
+
|
|
228
|
+
Girb::DebugSessionHistory.record_ai_question(question)
|
|
229
|
+
handle_ai_question(question)
|
|
230
|
+
pending_cmds = Girb::DebugIntegration.take_pending_debug_commands
|
|
231
|
+
if pending_cmds.any?
|
|
232
|
+
pending_cmds.each do |cmd|
|
|
233
|
+
result = super(cmd)
|
|
234
|
+
return result unless result == :retry
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
return :retry
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
if line.start_with?("qq ")
|
|
241
|
+
question = line.sub(/^qq\s+/, "").strip
|
|
242
|
+
return :retry if question.empty?
|
|
243
|
+
|
|
244
|
+
# セッション管理コマンド
|
|
245
|
+
if question.start_with?("session ")
|
|
246
|
+
handle_session_command(question.sub(/^session\s+/, ""))
|
|
247
|
+
return :retry
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
Girb::DebugSessionHistory.record_ai_question(question)
|
|
251
|
+
handle_ai_question(question)
|
|
252
|
+
pending_cmds = Girb::DebugIntegration.take_pending_debug_commands
|
|
253
|
+
if pending_cmds.any?
|
|
254
|
+
pending_cmds.each do |cmd|
|
|
255
|
+
result = super(cmd)
|
|
256
|
+
return result unless result == :retry
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
return :retry
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Auto-detect natural language (non-ASCII input) and route to AI
|
|
263
|
+
if line.match?(/[^\x00-\x7F]/)
|
|
264
|
+
question = line.strip
|
|
265
|
+
return :retry if question.empty?
|
|
266
|
+
|
|
267
|
+
Girb::DebugSessionHistory.record_ai_question(question)
|
|
268
|
+
handle_ai_question(question)
|
|
269
|
+
pending_cmds = Girb::DebugIntegration.take_pending_debug_commands
|
|
270
|
+
if pending_cmds.any?
|
|
271
|
+
pending_cmds.each do |cmd|
|
|
272
|
+
result = super(cmd)
|
|
273
|
+
return result unless result == :retry
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
return :retry
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Record regular debugger commands
|
|
280
|
+
Girb::DebugSessionHistory.record_command(line)
|
|
281
|
+
super
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
private
|
|
285
|
+
|
|
286
|
+
def handle_ai_continuation
|
|
287
|
+
current_binding = @tc&.current_frame&.eval_binding
|
|
288
|
+
unless current_binding
|
|
289
|
+
@ui.puts "[girb] Error: No current frame available"
|
|
290
|
+
return
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
context = Girb::DebugContextBuilder.new(current_binding).build
|
|
294
|
+
client = Girb::AiClient.new
|
|
295
|
+
continuation = "(auto-continue: The debug command has been executed. Analyze the new state and continue your task.)"
|
|
296
|
+
# Disable Ruby's Timeout during API call to avoid deadlock with debug gem's threading
|
|
297
|
+
with_timeout_disabled do
|
|
298
|
+
client.ask(continuation, context, binding: current_binding, debug_mode: true)
|
|
299
|
+
end
|
|
300
|
+
rescue StandardError => e
|
|
301
|
+
@ui.puts "[girb] Auto-continue error: #{e.message}"
|
|
302
|
+
Girb::DebugIntegration.auto_continue = false
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def handle_ai_question(question)
|
|
306
|
+
current_binding = @tc&.current_frame&.eval_binding
|
|
307
|
+
|
|
308
|
+
unless current_binding
|
|
309
|
+
puts "[girb] Error: No current frame available"
|
|
310
|
+
return
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# 初回のAI質問時にセッションを開始
|
|
314
|
+
Girb::DebugIntegration.start_session!
|
|
315
|
+
|
|
316
|
+
context = Girb::DebugContextBuilder.new(current_binding).build
|
|
317
|
+
client = Girb::AiClient.new
|
|
318
|
+
# Disable Ruby's Timeout during API call to avoid deadlock with debug gem's threading
|
|
319
|
+
with_timeout_disabled do
|
|
320
|
+
client.ask(question, context, binding: current_binding, debug_mode: true)
|
|
321
|
+
end
|
|
322
|
+
rescue Girb::ConfigurationError => e
|
|
323
|
+
puts "[girb] #{e.message}"
|
|
324
|
+
rescue StandardError => e
|
|
325
|
+
puts "[girb] Error: #{e.message}"
|
|
326
|
+
puts e.backtrace.first(3).join("\n") if Girb.configuration.debug
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Temporarily disable Ruby's Timeout module to avoid deadlock with debug gem
|
|
330
|
+
# The underlying socket has its own timeout, so this is safe
|
|
331
|
+
def with_timeout_disabled
|
|
332
|
+
return yield unless defined?(Timeout)
|
|
333
|
+
|
|
334
|
+
original_timeout = Timeout.method(:timeout)
|
|
335
|
+
Timeout.define_singleton_method(:timeout) do |_sec, _klass = nil, _message = nil, &block|
|
|
336
|
+
block.call
|
|
337
|
+
end
|
|
338
|
+
yield
|
|
339
|
+
ensure
|
|
340
|
+
Timeout.define_singleton_method(:timeout, original_timeout) if original_timeout
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def handle_ai_turn_limit_reached
|
|
344
|
+
current_binding = @tc&.current_frame&.eval_binding
|
|
345
|
+
return unless current_binding
|
|
346
|
+
|
|
347
|
+
context = Girb::DebugContextBuilder.new(current_binding).build
|
|
348
|
+
client = Girb::AiClient.new
|
|
349
|
+
limit_message = "(System: Auto-continue turn limit (#{MAX_AUTO_CONTINUE}) reached. " \
|
|
350
|
+
"Summarize your progress so far and tell the user what was accomplished. " \
|
|
351
|
+
"If the task is not complete, explain what remains and instruct the user " \
|
|
352
|
+
"to continue with a follow-up request.)"
|
|
353
|
+
with_timeout_disabled do
|
|
354
|
+
client.ask(limit_message, context, binding: current_binding, debug_mode: true)
|
|
355
|
+
end
|
|
356
|
+
rescue StandardError => e
|
|
357
|
+
puts "[girb] Auto-continue limit reached (#{MAX_AUTO_CONTINUE})"
|
|
358
|
+
puts "[girb] Error summarizing: #{e.message}" if Girb.configuration.debug
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def handle_ai_interrupted
|
|
362
|
+
puts "\n[girb] Interrupted by user (Ctrl+C)"
|
|
363
|
+
current_binding = @tc&.current_frame&.eval_binding
|
|
364
|
+
return unless current_binding
|
|
365
|
+
|
|
366
|
+
context = Girb::DebugContextBuilder.new(current_binding).build
|
|
367
|
+
client = Girb::AiClient.new
|
|
368
|
+
interrupt_message = "(System: User interrupted with Ctrl+C. " \
|
|
369
|
+
"Briefly summarize your progress so far. " \
|
|
370
|
+
"Tell the user where you stopped and how to continue if needed.)"
|
|
371
|
+
with_timeout_disabled do
|
|
372
|
+
client.ask(interrupt_message, context, binding: current_binding, debug_mode: true)
|
|
373
|
+
end
|
|
374
|
+
rescue StandardError => e
|
|
375
|
+
puts "[girb] Error summarizing: #{e.message}" if Girb.configuration.debug
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def setup_interrupt_handler
|
|
379
|
+
Girb::DebugIntegration.api_thread = Thread.current
|
|
380
|
+
@original_int_handler = trap("INT") do
|
|
381
|
+
Girb::DebugIntegration.interrupt!
|
|
382
|
+
# Raise Interrupt to break out of blocking IO operations
|
|
383
|
+
thread = Girb::DebugIntegration.api_thread
|
|
384
|
+
thread&.raise(Interrupt) if thread&.alive?
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def restore_interrupt_handler
|
|
389
|
+
if @original_int_handler
|
|
390
|
+
trap("INT", @original_int_handler)
|
|
391
|
+
@original_int_handler = nil
|
|
392
|
+
else
|
|
393
|
+
trap("INT", "DEFAULT")
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def handle_session_command(cmd)
|
|
398
|
+
case cmd.strip
|
|
399
|
+
when "clear"
|
|
400
|
+
Girb::SessionPersistence.clear_session
|
|
401
|
+
when "list"
|
|
402
|
+
sessions = Girb::SessionPersistence.list_sessions
|
|
403
|
+
if sessions.empty?
|
|
404
|
+
puts "[girb] No saved sessions"
|
|
405
|
+
else
|
|
406
|
+
puts "[girb] Saved sessions:"
|
|
407
|
+
sessions.each do |s|
|
|
408
|
+
puts " - #{s[:id]} (#{s[:message_count]} messages, saved: #{s[:saved_at]})"
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
when "status"
|
|
412
|
+
if Girb::SessionPersistence.current_session_id
|
|
413
|
+
puts "[girb] Current session: #{Girb::SessionPersistence.current_session_id}"
|
|
414
|
+
elsif Girb.debug_session
|
|
415
|
+
puts "[girb] Session configured: #{Girb.debug_session} (not started)"
|
|
416
|
+
else
|
|
417
|
+
puts "[girb] No session configured (use Girb.debug_session = 'name' to enable)"
|
|
418
|
+
end
|
|
419
|
+
else
|
|
420
|
+
puts "[girb] Unknown session command: #{cmd}"
|
|
421
|
+
puts "[girb] Available: clear, list, status"
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Girb
|
|
4
|
+
class DebugPromptBuilder
|
|
5
|
+
SYSTEM_PROMPT = <<~PROMPT
|
|
6
|
+
You are girb, an AI debugging assistant embedded in a Ruby debugger session.
|
|
7
|
+
You are integrated with Ruby's debug gem and can help developers debug their code.
|
|
8
|
+
|
|
9
|
+
## CRITICAL: Context Information
|
|
10
|
+
The user is stopped at a breakpoint or debugger statement.
|
|
11
|
+
You have access to the current execution context including:
|
|
12
|
+
- Local variables and their values
|
|
13
|
+
- Instance variables of the current object
|
|
14
|
+
- The current file and line number
|
|
15
|
+
- The call stack (backtrace)
|
|
16
|
+
|
|
17
|
+
## Language
|
|
18
|
+
Respond in the same language the user is using.
|
|
19
|
+
|
|
20
|
+
## Your Role
|
|
21
|
+
- Help debug issues by analyzing the current state
|
|
22
|
+
- Explain what the code is doing and why it might be failing
|
|
23
|
+
- Use tools to inspect objects, evaluate code, or read source files
|
|
24
|
+
- Provide actionable advice to fix issues
|
|
25
|
+
|
|
26
|
+
## When to Investigate Proactively
|
|
27
|
+
When the user asks about code, debugging, variables, errors, or anything related to their program,
|
|
28
|
+
you should investigate before responding:
|
|
29
|
+
- Use `read_file` to read the source file shown in "Source Location" if relevant to the question
|
|
30
|
+
- Use `evaluate_code` to run and verify code rather than guessing or reasoning about results
|
|
31
|
+
- NEVER ask the user for code, file names, or variable definitions that you can look up
|
|
32
|
+
yourself with `read_file`, `evaluate_code`, `inspect_object`, or `find_file`
|
|
33
|
+
|
|
34
|
+
However, for simple greetings or conversational messages (e.g., "hello", "hi", "こんにちは", "thanks"),
|
|
35
|
+
just respond naturally without using tools. Not every message requires investigation.
|
|
36
|
+
|
|
37
|
+
## CRITICAL: Variable Persistence Across Frames
|
|
38
|
+
Local variables created via `evaluate_code` do NOT persist after `step`, `next`, or `continue`.
|
|
39
|
+
When the program moves to a new frame, those local variables are lost.
|
|
40
|
+
|
|
41
|
+
To track values across multiple breakpoints or frames, use:
|
|
42
|
+
- Instance variables: `@x_values = []` then `@x_values << x`
|
|
43
|
+
- Global variables: `$x_values = []` then `$x_values << x`
|
|
44
|
+
|
|
45
|
+
## Efficiency: Prefer Conditional Breakpoints for Loops
|
|
46
|
+
When tracking variables through many iterations (loops, recursion), avoid repeated `next`/`step`
|
|
47
|
+
commands. Each step requires an API call, which is slow. Use conditional breakpoints instead:
|
|
48
|
+
|
|
49
|
+
**Efficient approach for loops with many iterations:**
|
|
50
|
+
1. `evaluate_code("$tracked = []")` - initialize tracking array
|
|
51
|
+
2. Use a conditional breakpoint that records AND stops on condition:
|
|
52
|
+
`break file.rb:10 if: ($tracked << x; x == 1)`
|
|
53
|
+
This appends x to $tracked on EVERY hit, but only stops when x == 1.
|
|
54
|
+
3. `continue` - run through all iterations at full speed
|
|
55
|
+
4. When stopped (or at end): `evaluate_code("$tracked")` to see all collected values
|
|
56
|
+
|
|
57
|
+
This completes in 2-3 API turns instead of many turns with repeated stepping.
|
|
58
|
+
|
|
59
|
+
**When to use repeated stepping (next/step):**
|
|
60
|
+
- Understanding complex logic flow (few lines)
|
|
61
|
+
- Checking which branch is taken
|
|
62
|
+
- Loops with only 2-3 iterations
|
|
63
|
+
- User explicitly wants to see execution step by step
|
|
64
|
+
|
|
65
|
+
**When to use conditional breakpoints:**
|
|
66
|
+
- Loops with many iterations (5+)
|
|
67
|
+
- "Track variable X until condition Y" requests
|
|
68
|
+
- "Find when X becomes Y" requests
|
|
69
|
+
- Collecting history of values
|
|
70
|
+
|
|
71
|
+
## CRITICAL: Executing Debugger Commands
|
|
72
|
+
When the user asks you to perform a debugging action (e.g., "go to the next line", "step into",
|
|
73
|
+
"continue", "advance to line N", "set a breakpoint"), you MUST use the `run_debug_command` tool.
|
|
74
|
+
Do NOT just print or suggest the command as text — actually call the tool.
|
|
75
|
+
You can also use the `evaluate_code` tool to run Ruby expressions in the current context.
|
|
76
|
+
|
|
77
|
+
Available debugger commands for run_debug_command:
|
|
78
|
+
- `step` / `s`: Step into method calls
|
|
79
|
+
- `next` / `n`: Step over to next line
|
|
80
|
+
- `continue` / `c`: Continue execution
|
|
81
|
+
- `finish`: Run until current method returns
|
|
82
|
+
- `up` / `down`: Navigate the call stack
|
|
83
|
+
- `break <file>:<line>`: Set a breakpoint (e.g., `break sample.rb:14`)
|
|
84
|
+
- `info locals`: Show local variables
|
|
85
|
+
- `pp <expr>`: Pretty print an expression
|
|
86
|
+
|
|
87
|
+
IMPORTANT: For conditional breakpoints, use `if:` (with colon), NOT `if` (without colon).
|
|
88
|
+
Example: `break sample.rb:14 if: x == 1`
|
|
89
|
+
|
|
90
|
+
IMPORTANT: Each `run_debug_command` call must contain exactly ONE debugger command.
|
|
91
|
+
NEVER combine multiple commands with `;` or append debugger commands to breakpoint conditions.
|
|
92
|
+
BAD: `break sample.rb:14 if: x == 1; continue` ("; continue" becomes part of the Ruby condition and causes an error)
|
|
93
|
+
GOOD: Call `run_debug_command("break sample.rb:14 if: x == 1")` then `run_debug_command("c")` separately.
|
|
94
|
+
|
|
95
|
+
## Response Guidelines
|
|
96
|
+
- Keep responses concise and actionable
|
|
97
|
+
- Focus on the immediate debugging task
|
|
98
|
+
- When the user requests a debugger action, execute it via run_debug_command — do not just describe it
|
|
99
|
+
- NEVER repeat the same failed action. If a tool call fails, analyze the error and try a different approach
|
|
100
|
+
- If you encounter an error about undefined variables after continue/step, remember to use instance or global variables
|
|
101
|
+
- IMPORTANT: When a task is complete (tracking finished, script ended, etc.), ALWAYS report the results.
|
|
102
|
+
Don't just execute commands and stop — check the collected data and summarize findings for the user.
|
|
103
|
+
For example, after tracking variables: use evaluate_code to retrieve $tracked and present the results.
|
|
104
|
+
|
|
105
|
+
## Available Tools
|
|
106
|
+
Use tools to inspect the runtime state:
|
|
107
|
+
- evaluate_code: Execute Ruby code in the current context
|
|
108
|
+
- inspect_object: Get detailed information about objects
|
|
109
|
+
- get_source: Read method or class source code
|
|
110
|
+
- list_methods: List available methods on an object
|
|
111
|
+
- read_file: Read source files
|
|
112
|
+
- find_file: Find files in the project
|
|
113
|
+
- get_session_history: Get past debugger commands and AI conversations
|
|
114
|
+
- run_debug_command: Execute a debugger command (n, s, c, finish, up, down, break, info, bt, etc.)
|
|
115
|
+
|
|
116
|
+
## Session History
|
|
117
|
+
The "Session History" section in the context shows recent debugger commands and AI conversations.
|
|
118
|
+
Use this to understand the user's past actions and questions. Format:
|
|
119
|
+
- [cmd] ... : Debugger command entered by user
|
|
120
|
+
- [ai] Q: ... A: ... : Previous AI question and response
|
|
121
|
+
|
|
122
|
+
## Interactive Debugging with auto_continue
|
|
123
|
+
When you need to execute a debugger command AND see the result before deciding your next action,
|
|
124
|
+
use `run_debug_command` with `auto_continue: true`.
|
|
125
|
+
|
|
126
|
+
After the command executes and the program stops at a new point, you will be automatically
|
|
127
|
+
re-invoked with the updated debug context (new file/line, new variable values).
|
|
128
|
+
You can then inspect variables, evaluate code, and decide whether to continue stepping or
|
|
129
|
+
give your final answer.
|
|
130
|
+
|
|
131
|
+
Use `auto_continue: true` when:
|
|
132
|
+
- Stepping through code to find where a variable changes
|
|
133
|
+
- Continuing to a breakpoint and then analyzing the state
|
|
134
|
+
- Any scenario where you need to see the result of a navigation command
|
|
135
|
+
- When the user asks you to track/collect data and report results — you need to be re-invoked
|
|
136
|
+
after the program stops so you can check the collected data and report back
|
|
137
|
+
|
|
138
|
+
Do NOT use `auto_continue: true` when:
|
|
139
|
+
- You've already collected and reported all the information the user asked for
|
|
140
|
+
- The user explicitly asks to just run a command without analysis
|
|
141
|
+
|
|
142
|
+
You can call `run_debug_command` multiple times in a single turn to batch commands.
|
|
143
|
+
Non-navigation commands (break, info, bt) should come before navigation commands (step, next, continue).
|
|
144
|
+
PROMPT
|
|
145
|
+
|
|
146
|
+
def initialize(question, context)
|
|
147
|
+
@question = question
|
|
148
|
+
@context = context
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def system_prompt
|
|
152
|
+
custom = Girb.configuration&.custom_prompt
|
|
153
|
+
if custom && !custom.empty?
|
|
154
|
+
"#{SYSTEM_PROMPT}\n\n## User-Defined Instructions\n#{custom}"
|
|
155
|
+
else
|
|
156
|
+
SYSTEM_PROMPT
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def user_message
|
|
161
|
+
<<~MSG
|
|
162
|
+
## Current Debug Context
|
|
163
|
+
#{build_context_section}
|
|
164
|
+
|
|
165
|
+
## Question
|
|
166
|
+
#{@question}
|
|
167
|
+
MSG
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
def build_context_section
|
|
173
|
+
<<~CONTEXT
|
|
174
|
+
### Source Location
|
|
175
|
+
#{format_source_location}
|
|
176
|
+
|
|
177
|
+
### Local Variables
|
|
178
|
+
#{format_locals}
|
|
179
|
+
|
|
180
|
+
### Instance Variables
|
|
181
|
+
#{format_instance_variables}
|
|
182
|
+
|
|
183
|
+
### Current Object (self)
|
|
184
|
+
#{format_self_info}
|
|
185
|
+
|
|
186
|
+
### Backtrace
|
|
187
|
+
#{format_backtrace}
|
|
188
|
+
|
|
189
|
+
### Session History (recent commands and AI conversations)
|
|
190
|
+
#{format_session_history}
|
|
191
|
+
CONTEXT
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def format_source_location
|
|
195
|
+
loc = @context[:source_location]
|
|
196
|
+
return "(unknown)" unless loc
|
|
197
|
+
|
|
198
|
+
"File: #{loc[:file]}\nLine: #{loc[:line]}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def format_locals
|
|
202
|
+
locals = @context[:local_variables]
|
|
203
|
+
return "(none)" if locals.nil? || locals.empty?
|
|
204
|
+
|
|
205
|
+
locals.map { |name, value| "- #{name}: #{value}" }.join("\n")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def format_instance_variables
|
|
209
|
+
ivars = @context[:instance_variables]
|
|
210
|
+
return "(none)" if ivars.nil? || ivars.empty?
|
|
211
|
+
|
|
212
|
+
ivars.map { |name, value| "- #{name}: #{value}" }.join("\n")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def format_self_info
|
|
216
|
+
info = @context[:self_info]
|
|
217
|
+
return "(unknown)" unless info
|
|
218
|
+
|
|
219
|
+
lines = ["Class: #{info[:class]}"]
|
|
220
|
+
lines << "inspect: #{info[:inspect]}"
|
|
221
|
+
if info[:methods]&.any?
|
|
222
|
+
lines << "Defined methods: #{info[:methods].join(', ')}"
|
|
223
|
+
end
|
|
224
|
+
lines.join("\n")
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def format_backtrace
|
|
228
|
+
bt = @context[:backtrace]
|
|
229
|
+
return "(not available)" unless bt
|
|
230
|
+
|
|
231
|
+
bt
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def format_session_history
|
|
235
|
+
history = @context[:session_history]
|
|
236
|
+
return "(no history yet)" if history.nil? || history.empty?
|
|
237
|
+
|
|
238
|
+
history
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|