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.
@@ -2,8 +2,9 @@
2
2
 
3
3
  module Girb
4
4
  class PromptBuilder
5
- SYSTEM_PROMPT = <<~PROMPT
6
- You are girb, an AI assistant embedded in a Ruby developer's IRB session.
5
+ # Common prompt shared across all IRB modes
6
+ COMMON_PROMPT = <<~PROMPT
7
+ You are girb, an AI assistant embedded in a Ruby developer's session.
7
8
 
8
9
  ## CRITICAL: Prompt Information Takes Highest Priority
9
10
  Information in this system prompt and "User-Defined Instructions" section
@@ -14,36 +15,15 @@ module Girb
14
15
  ## Language
15
16
  Respond in the same language the user is using. Detect the user's language from their question and match it.
16
17
 
17
- ## Important: Understand the IRB Session Context
18
- The user is interactively executing code in IRB and asking questions within that flow.
19
- "Session History" contains the code the user has executed and past AI conversations in chronological order.
20
- Always interpret questions in the context of this history.
21
-
22
- For example, if the history shows:
23
- 1: a = 1
24
- 2: b = 2
25
- 3: [USER] What will z be if I continue with c = 3 and beyond?
26
- The user is asking about the value of z when continuing the pattern a=1, b=2, c=3... (answer: z=26).
27
-
28
- ## Your Role
29
- - Strive to understand the user's true intent and background
30
- - Don't just answer the question; understand what they're trying to achieve and what challenges they face
31
- - Analyze session history to understand what the user is trying to do
32
- - Utilize the current execution context (variables, object state, exceptions)
33
- - Provide specific, practical answers to questions
34
- - Use tools to execute and verify code as needed
35
-
36
- ## You May Ask Clarifying Questions
37
- When you have doubts, ask the user about preconditions or unclear points.
38
- - When multiple interpretations are possible: Confirm which interpretation is correct
39
- - When preconditions are unclear: Ask what they're aiming for, what environment they're assuming
40
- - When information is insufficient: Prompt for the full error message or related code
41
- Asking questions increases dialogue turns but reduces misunderstandings and enables more accurate answers.
18
+ ## Clarifying Questions (Use Sparingly)
19
+ Only ask the user for clarification AFTER you have already investigated using tools.
20
+ - First: read the source file, check variables, run code
21
+ - Then: if the intent is still ambiguous after investigation, ask a focused question
42
22
 
43
23
  ## Response Guidelines
44
24
  - Keep responses concise and practical
45
25
  - Read patterns and intentions; handle hypothetical questions
46
- - Code examples should use variables and objects from the current IRB context and be directly executable by pasting into IRB
26
+ - Code examples should use variables and objects from the current context and be directly executable
47
27
 
48
28
  ## Debugging Support on Errors
49
29
  When users encounter errors, actively support debugging.
@@ -51,11 +31,119 @@ module Girb
51
31
  - Suggest ways to inspect related code (e.g., using the inspect_object tool)
52
32
  - Guide them step-by-step toward writing more robust code
53
33
 
34
+ ## CRITICAL: Proactive Investigation — Act First, Don't Ask
35
+ You MUST investigate before asking the user for information.
36
+ - Use `evaluate_code` to run and verify code rather than guessing or reasoning about results.
37
+ - NEVER ask the user for code, file names, or variable definitions that you can look up
38
+ yourself with `read_file`, `evaluate_code`, `inspect_object`, or `find_file`.
39
+
54
40
  ## Available Tools
55
41
  Use tools to inspect variables in detail, retrieve source code, and execute code.
56
42
  Actively use the evaluate_code tool especially for verifying hypotheses and calculations.
57
43
  PROMPT
58
44
 
45
+ # Prompt specific to breakpoint mode (binding.girb / binding.irb)
46
+ BREAKPOINT_PROMPT = <<~PROMPT
47
+ ## Mode: Breakpoint (binding.girb)
48
+ You are at a BREAKPOINT in the user's Ruby script. Execution is paused at this exact line.
49
+
50
+ ### CRITICAL: Understanding Breakpoint Context
51
+ - Code BEFORE this line has already executed (variables are set)
52
+ - Code AFTER this line has NOT executed yet
53
+ - The user's questions about "the code", "this loop", "this method" refer to the code in the SOURCE FILE
54
+ - ALWAYS read the source file FIRST using `read_file` to understand what code exists
55
+
56
+ ### Your Primary Task
57
+ - Help the user understand, debug, or simulate the code that is ABOUT TO execute
58
+ - When asked to "run the code" or "execute this loop", execute the ACTUAL code from the file
59
+ - When asked to track variables, run the actual code and report real results
60
+ - NEVER invent or substitute code - always use what's in the file
61
+
62
+ ### Example: User says "run this loop and track x"
63
+ 1. Read the source file to see the actual loop code
64
+ 2. Execute that exact code using evaluate_code
65
+ 3. Report the actual results
66
+
67
+ ### WRONG approach:
68
+ - Guessing what the code might do
69
+ - Writing your own version of the code
70
+ - Asking the user what the code is when you can read the file
71
+
72
+ ### Debug Commands (use run_debug_command tool)
73
+ IRB integrates with debug gem. Use `run_debug_command` tool to execute:
74
+ - `next` / `n`: Step over to next line
75
+ - `step` / `s`: Step into method calls
76
+ - `continue` / `c`: Continue execution
77
+ - `finish`: Run until current method returns
78
+ - `break <file>:<line>`: Set a breakpoint
79
+ - `backtrace` / `bt`: Show call stack
80
+ - `info`: Show local variables
81
+
82
+ When the user asks for step-by-step execution, use `run_debug_command` with `auto_continue: true`
83
+ to step through the code and be re-invoked to see the results.
84
+ PROMPT
85
+
86
+ # Prompt specific to interactive IRB mode (girb command)
87
+ INTERACTIVE_IRB_PROMPT = <<~PROMPT
88
+ ## Mode: Interactive IRB Session
89
+ The user is in an interactive IRB session, typing code and questions directly.
90
+
91
+ ### Understanding the Session
92
+ - "Session History" contains the code the user has executed and past AI conversations
93
+ - Always interpret questions in the context of this history
94
+ - Variables and objects from past commands are available in the current context
95
+
96
+ ### Example Context
97
+ If the history shows:
98
+ 1: a = 1
99
+ 2: b = 2
100
+ 3: [USER] What will z be if I continue with c = 3 and beyond?
101
+ The user is asking about the value of z when continuing the pattern a=1, b=2, c=3... (answer: z=26).
102
+
103
+ ### Your Role
104
+ - Help with code exploration and experimentation
105
+ - Answer questions about Ruby, gems, and the current session state
106
+ - Assist with building and testing code interactively
107
+ PROMPT
108
+
109
+ # Prompt specific to Rails console mode
110
+ RAILS_CONSOLE_PROMPT = <<~PROMPT
111
+ ## Mode: Rails Console
112
+ The user is in a Rails console with full access to the application's models and services.
113
+
114
+ ### Rails-Specific Capabilities
115
+ - You can query ActiveRecord models directly
116
+ - Use `model_info` tool to get schema information
117
+ - Use `query_model` tool to execute database queries safely
118
+ - Access to Rails helpers, routes, and application configuration
119
+
120
+ ### Best Practices
121
+ - Be careful with destructive operations (update!, destroy, etc.) - warn the user
122
+ - Use transactions when demonstrating data modifications
123
+ - Suggest using `find_by` or `where` instead of `find` to avoid exceptions
124
+ - Remember that console changes affect the real database (unless in sandbox mode)
125
+ PROMPT
126
+
127
+ # Autonomous investigation prompt (shared)
128
+ CONTINUE_ANALYSIS_PROMPT = <<~PROMPT
129
+ ## Autonomous Investigation with continue_analysis
130
+ When you need to execute code that changes state AND then see the full updated context
131
+ (all local variables, instance variables, last value, etc.), use the `continue_analysis` tool.
132
+
133
+ After you call `continue_analysis`, your current response will be sent, and then you will be
134
+ automatically re-invoked with a refreshed context showing all current variable values.
135
+
136
+ ### When to use continue_analysis:
137
+ - After evaluating code that modifies variables, when you need to see the full picture
138
+ - When iteratively debugging: change something → check state → change more
139
+ - When you need to verify side effects of an operation across multiple variables
140
+
141
+ ### When NOT to use continue_analysis:
142
+ - When you can get the information you need with evaluate_code or inspect_object directly
143
+ - When you've found your answer and want to report to the user
144
+ - For simple one-shot investigations
145
+ PROMPT
146
+
59
147
  def initialize(question, context)
60
148
  @question = question
61
149
  @context = context
@@ -75,18 +163,20 @@ module Girb
75
163
 
76
164
  # System prompt (shared across conversation)
77
165
  def system_prompt
166
+ prompt = COMMON_PROMPT + "\n" + mode_specific_prompt + "\n" + CONTINUE_ANALYSIS_PROMPT
167
+
78
168
  custom = Girb.configuration&.custom_prompt
79
169
  if custom && !custom.empty?
80
- "#{SYSTEM_PROMPT}\n\n## User-Defined Instructions\n#{custom}"
170
+ prompt + "\n\n## User-Defined Instructions\n#{custom}"
81
171
  else
82
- SYSTEM_PROMPT
172
+ prompt
83
173
  end
84
174
  end
85
175
 
86
176
  # User message (context + question)
87
177
  def user_message
88
178
  <<~MSG
89
- ## Current IRB Context
179
+ ## Current Context
90
180
  #{build_context_section}
91
181
 
92
182
  ## Question
@@ -96,29 +186,49 @@ module Girb
96
186
 
97
187
  private
98
188
 
99
- def build_context_section
100
- <<~CONTEXT
101
- ### Session History (Previous IRB Inputs)
102
- Below is the code the user has executed so far. The question is asked within this flow.
103
- #{format_session_history}
189
+ def mode_specific_prompt
190
+ case detect_mode
191
+ when :breakpoint
192
+ BREAKPOINT_PROMPT
193
+ when :rails
194
+ RAILS_CONSOLE_PROMPT
195
+ else
196
+ INTERACTIVE_IRB_PROMPT
197
+ end
198
+ end
199
+
200
+ def detect_mode
201
+ loc = @context[:source_location]
104
202
 
105
- ### Current Local Variables
106
- #{format_locals}
203
+ # Check for breakpoint mode: source is a real file (not irb/eval)
204
+ if loc && loc[:file]
205
+ file = loc[:file].to_s
206
+ unless file.start_with?("(") || file.include?("irb") || file.include?("eval")
207
+ return :breakpoint
208
+ end
209
+ end
107
210
 
108
- ### Last Evaluation Result
109
- #{@context[:last_value] || "(none)"}
211
+ # Check for Rails mode
212
+ return :rails if defined?(Rails)
110
213
 
111
- ### Last Exception
112
- #{format_exception}
214
+ # Default: interactive IRB
215
+ :interactive
216
+ end
113
217
 
114
- ### Methods Defined in IRB
115
- #{format_method_definitions}
116
- CONTEXT
218
+ def build_context_section
219
+ sections = []
220
+ sections << "### Source Location\n#{format_source_location}"
221
+ sections << "### Session History (Previous Inputs)\n#{format_session_history}"
222
+ sections << "### Current Local Variables\n#{format_locals}"
223
+ sections << "### Last Evaluation Result\n#{@context[:last_value] || "(none)"}"
224
+ sections << "### Last Exception\n#{format_exception}"
225
+ sections << "### Methods Defined in Session\n#{format_method_definitions}"
226
+ sections.join("\n\n")
117
227
  end
118
228
 
119
229
  def format_source_location
120
230
  loc = @context[:source_location]
121
- return "(unknown)" unless loc
231
+ return "(interactive session)" unless loc
122
232
 
123
233
  "File: #{loc[:file]}\nLine: #{loc[:line]}"
124
234
  end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+
6
+ module Girb
7
+ # デバッグセッションの会話履歴を永続化するクラス
8
+ # 明示的にGirb.debug_sessionを設定した場合のみ保存される
9
+ class SessionPersistence
10
+ SESSIONS_DIR = ".girb/sessions"
11
+
12
+ class << self
13
+ attr_accessor :current_session_id
14
+
15
+ # セッションが有効か(明示的にIDが指定されているか)
16
+ def enabled?
17
+ !!Girb.debug_session
18
+ end
19
+
20
+ # セッションディレクトリを取得(プロジェクトルートから)
21
+ def sessions_dir
22
+ # カレントディレクトリから.girbディレクトリを探す
23
+ dir = Dir.pwd
24
+ while dir != "/"
25
+ girb_dir = File.join(dir, ".girb")
26
+ if Dir.exist?(girb_dir)
27
+ return File.join(girb_dir, "sessions")
28
+ end
29
+ dir = File.dirname(dir)
30
+ end
31
+
32
+ # 見つからなければカレントディレクトリに作成
33
+ File.join(Dir.pwd, SESSIONS_DIR)
34
+ end
35
+
36
+ # セッションファイルのパスを取得
37
+ def session_file_path(session_id)
38
+ File.join(sessions_dir, "#{session_id}.json")
39
+ end
40
+
41
+ # セッションを開始(既存があれば読み込み)
42
+ # Girb.debug_sessionが設定されている場合のみ有効
43
+ def start_session
44
+ return unless enabled?
45
+
46
+ @current_session_id = Girb.debug_session
47
+
48
+ file_path = session_file_path(@current_session_id)
49
+ if File.exist?(file_path)
50
+ load_session(file_path)
51
+ puts "[girb] Resumed session: #{@current_session_id}"
52
+ else
53
+ ConversationHistory.reset!
54
+ puts "[girb] New session: #{@current_session_id}"
55
+ end
56
+
57
+ @current_session_id
58
+ end
59
+
60
+ # セッションを保存
61
+ def save_session
62
+ return unless enabled? && @current_session_id
63
+
64
+ file_path = session_file_path(@current_session_id)
65
+ FileUtils.mkdir_p(File.dirname(file_path))
66
+
67
+ data = {
68
+ session_id: @current_session_id,
69
+ saved_at: Time.now.iso8601,
70
+ messages: serialize_messages
71
+ }
72
+
73
+ File.write(file_path, JSON.pretty_generate(data))
74
+ puts "[girb] Session saved: #{@current_session_id}"
75
+ rescue => e
76
+ puts "[girb] Failed to save session: #{e.message}"
77
+ end
78
+
79
+ # セッションを読み込み
80
+ def load_session(file_path)
81
+ data = JSON.parse(File.read(file_path), symbolize_names: true)
82
+
83
+ ConversationHistory.reset!
84
+ deserialize_messages(data[:messages])
85
+
86
+ message_count = data[:messages]&.size || 0
87
+ puts "[girb] Loaded #{message_count} messages from previous session"
88
+ rescue => e
89
+ puts "[girb] Failed to load session: #{e.message}"
90
+ ConversationHistory.reset!
91
+ end
92
+
93
+ # 現在のセッションをクリア(ファイルも削除)
94
+ def clear_session
95
+ if @current_session_id
96
+ delete_session(@current_session_id)
97
+ @current_session_id = nil
98
+ end
99
+ ConversationHistory.reset!
100
+ puts "[girb] Session cleared"
101
+ end
102
+
103
+ # セッション一覧を取得
104
+ def list_sessions
105
+ dir = sessions_dir
106
+ return [] unless Dir.exist?(dir)
107
+
108
+ Dir.glob(File.join(dir, "*.json")).map do |file|
109
+ data = JSON.parse(File.read(file), symbolize_names: true)
110
+ {
111
+ id: data[:session_id],
112
+ saved_at: data[:saved_at],
113
+ message_count: data[:messages]&.size || 0
114
+ }
115
+ rescue
116
+ nil
117
+ end.compact
118
+ end
119
+
120
+ # セッションを削除
121
+ def delete_session(session_id)
122
+ file_path = session_file_path(session_id)
123
+ if File.exist?(file_path)
124
+ File.delete(file_path)
125
+ puts "[girb] Session deleted: #{session_id}"
126
+ true
127
+ else
128
+ puts "[girb] Session not found: #{session_id}"
129
+ false
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def serialize_messages
136
+ ConversationHistory.messages.map do |msg|
137
+ {
138
+ role: msg.role,
139
+ content: msg.content,
140
+ tool_calls: msg.tool_calls
141
+ }
142
+ end
143
+ end
144
+
145
+ def deserialize_messages(messages)
146
+ return unless messages
147
+
148
+ messages.each do |msg|
149
+ case msg[:role]
150
+ when "user"
151
+ ConversationHistory.add_user_message(msg[:content])
152
+ when "model"
153
+ # tool_callsがある場合は先にpending_tool_callsに追加
154
+ if msg[:tool_calls]&.any?
155
+ msg[:tool_calls].each do |tc|
156
+ ConversationHistory.add_tool_call(
157
+ tc[:name],
158
+ tc[:args],
159
+ tc[:result],
160
+ id: tc[:id]
161
+ )
162
+ end
163
+ end
164
+ ConversationHistory.add_assistant_message(msg[:content])
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Girb
6
+ module Tools
7
+ class ContinueAnalysis < Base
8
+ class << self
9
+ def description
10
+ "Request to be re-invoked with a refreshed context (updated local variables, " \
11
+ "instance variables, last value, etc.). Use this after executing code that " \
12
+ "changes state, when you need to see the full updated picture before deciding " \
13
+ "your next action."
14
+ end
15
+
16
+ def parameters
17
+ {
18
+ type: "object",
19
+ properties: {
20
+ reason: {
21
+ type: "string",
22
+ description: "Brief description of why you need a context refresh and what you plan to check next."
23
+ }
24
+ },
25
+ required: ["reason"]
26
+ }
27
+ end
28
+
29
+ def available?
30
+ # In debug mode, use run_debug_command with auto_continue instead
31
+ !defined?(DEBUGGER__)
32
+ end
33
+ end
34
+
35
+ def execute(binding, reason:)
36
+ Girb::AutoContinue.request!
37
+ {
38
+ success: true,
39
+ message: "You will be re-invoked with updated context after this response.",
40
+ reason: reason
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../debug_session_history"
5
+ require_relative "../conversation_history"
6
+ require_relative "../session_persistence"
7
+
8
+ module Girb
9
+ module Tools
10
+ class DebugSessionHistoryTool < Base
11
+ class << self
12
+ def name
13
+ "get_session_history"
14
+ end
15
+
16
+ def description
17
+ "Get session history including AI conversations from previous sessions (if persisted) and current session commands. " \
18
+ "Use this to recall past conversations and debug commands."
19
+ end
20
+
21
+ def parameters
22
+ {
23
+ type: "object",
24
+ properties: {
25
+ action: {
26
+ type: "string",
27
+ enum: %w[full_history list_ai_conversations],
28
+ description: "Action: full_history (all commands and AI conversations), list_ai_conversations (AI Q&A only)"
29
+ },
30
+ count: {
31
+ type: "integer",
32
+ description: "Number of recent entries to retrieve (default: 20)"
33
+ }
34
+ },
35
+ required: ["action"]
36
+ }
37
+ end
38
+
39
+ def available?
40
+ defined?(DEBUGGER__)
41
+ end
42
+ end
43
+
44
+ def execute(_binding, action:, count: 20)
45
+ case action
46
+ when "full_history"
47
+ get_full_history(count)
48
+ when "list_ai_conversations"
49
+ list_ai_conversations(count)
50
+ else
51
+ { error: "Unknown action: #{action}. Use 'full_history' or 'list_ai_conversations'." }
52
+ end
53
+ rescue StandardError => e
54
+ { error: "#{e.class}: #{e.message}" }
55
+ end
56
+
57
+ private
58
+
59
+ def get_full_history(count)
60
+ result = {}
61
+
62
+ # 永続化されたセッションからの会話履歴
63
+ persisted = get_persisted_conversations
64
+ if persisted.any?
65
+ result[:persisted_conversations] = persisted
66
+ end
67
+
68
+ # 現在のセッションのデバッグ履歴
69
+ history = DebugSessionHistory.format_history(count)
70
+ if history && !history.empty?
71
+ result[:current_session_history] = history
72
+ end
73
+
74
+ if result.empty?
75
+ { message: "No history available" }
76
+ else
77
+ result
78
+ end
79
+ end
80
+
81
+ def list_ai_conversations(count = 20)
82
+ all_conversations = []
83
+
84
+ # 永続化されたセッションからの会話
85
+ persisted = get_persisted_conversations
86
+ all_conversations.concat(persisted)
87
+
88
+ # 現在のセッションの会話
89
+ current = DebugSessionHistory.ai_conversations.map do |c|
90
+ {
91
+ question: c.content,
92
+ response: c.response || "(pending)",
93
+ source: "current_session"
94
+ }
95
+ end
96
+ all_conversations.concat(current)
97
+
98
+ if all_conversations.any?
99
+ # 最新のcount件に制限
100
+ limited = all_conversations.last(count)
101
+ {
102
+ total_count: all_conversations.size,
103
+ showing: limited.size,
104
+ conversations: limited.map do |c|
105
+ response = c[:response] || ""
106
+ response_preview = response.length > 200 ? "#{response[0, 200]}..." : response
107
+ {
108
+ question: c[:question],
109
+ response_preview: response_preview,
110
+ source: c[:source] || "persisted"
111
+ }
112
+ end
113
+ }
114
+ else
115
+ { message: "No AI conversations in session history" }
116
+ end
117
+ end
118
+
119
+ def get_persisted_conversations
120
+ conversations = []
121
+ ConversationHistory.messages.each do |msg|
122
+ if msg.role == "user"
123
+ conversations << { question: msg.content, source: "persisted" }
124
+ elsif msg.role == "model" && conversations.last && !conversations.last[:response]
125
+ conversations.last[:response] = msg.content
126
+ end
127
+ end
128
+ conversations
129
+ end
130
+ end
131
+ end
132
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "stringio"
3
4
  require_relative "base"
4
5
 
5
6
  module Girb
@@ -26,17 +27,37 @@ module Girb
26
27
  end
27
28
 
28
29
  def execute(binding, code:)
29
- result = binding.eval(code)
30
- {
30
+ captured_output = StringIO.new
31
+ original_stdout = $stdout
32
+ $stdout = captured_output
33
+
34
+ begin
35
+ result = binding.eval(code)
36
+ ensure
37
+ $stdout = original_stdout
38
+ end
39
+
40
+ stdout_str = captured_output.string
41
+ # Also print captured output to the real console for user visibility
42
+ print stdout_str unless stdout_str.empty?
43
+
44
+ response = {
31
45
  code: code,
32
46
  result: safe_inspect(result),
33
47
  result_class: result.class.name,
34
48
  success: true
35
49
  }
50
+ response[:stdout] = stdout_str unless stdout_str.empty?
51
+ response
36
52
  rescue SyntaxError => e
53
+ $stdout = original_stdout if $stdout != original_stdout
37
54
  { code: code, error: "Syntax error: #{e.message}", success: false }
38
55
  rescue StandardError => e
39
- { code: code, error: "#{e.class}: #{e.message}", backtrace: e.backtrace&.first(5), success: false }
56
+ $stdout = original_stdout if $stdout != original_stdout
57
+ stdout_str = captured_output&.string
58
+ response = { code: code, error: "#{e.class}: #{e.message}", backtrace: e.backtrace&.first(5), success: false }
59
+ response[:stdout] = stdout_str if stdout_str && !stdout_str.empty?
60
+ response
40
61
  end
41
62
  end
42
63
  end