console_agent 0.5.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35e4cf8ecceb5241ce1b18f2cac6d8a235b3185f7839b2143eca6233390375d5
4
- data.tar.gz: 7ebfc01003d9995ce65645a1f15955747cd59197c891e294d2152f3101fd583c
3
+ metadata.gz: 662ece5e5732e350b22871c8029421edf06ed4d9f98183f5cee7e95a3c1099f6
4
+ data.tar.gz: 483642598de39d23ace26f5d90863a8712b8799357c7809b2ba770f2bdc2522a
5
5
  SHA512:
6
- metadata.gz: 1bfdf0d1b5a6278101d7cda640ac710a1aef589e5a1adc4194876404d897a53bcfa27cd423029abc76a117eb6ab2ba13a14e605c4447c1452ab78d0f7b98b960
7
- data.tar.gz: 7589590b8dcbfd4268070ce59da381741ca1643152b2689829d741a6cc325e7d9130576629a3155ba0267fb0ea86623a584f9a6e6c2956143e043da43e551501
6
+ metadata.gz: 5095d8f4cb0706e84c81be1b4a859de6ac48336ccb9c950feeb0dcf0df676d78fddc53a2314a21cbe6ee9d79b808f5edf0d854f3262b72c5befbb4420d9b979b
7
+ data.tar.gz: 4c96f0e1664c1357cc7ec3f53aada9d1dcf4e05cbc92e19edfd052b2e99c75d1596d5a49440b8b9478fd576140b36929589a87e009dd971b4599cfa8ab149520
data/README.md CHANGED
@@ -1,40 +1,6 @@
1
1
  # ConsoleAgent
2
2
 
3
- Claude Code, embedded in your Rails console.
4
-
5
- Ask questions in plain English. The AI explores your schema, models, and source code on its own, then writes and runs the Ruby code for you.
6
-
7
- ## Install
8
-
9
- ```ruby
10
- # Gemfile
11
- gem 'console_agent', group: :development
12
- ```
13
-
14
- ```bash
15
- bundle install
16
- rails generate console_agent:install
17
- ```
18
-
19
- Set your API key in the generated initializer or as an env var (`ANTHROPIC_API_KEY`):
20
-
21
- ```ruby
22
- # config/initializers/console_agent.rb
23
- ConsoleAgent.configure do |config|
24
- config.api_key = 'sk-ant-...'
25
- end
26
- ```
27
-
28
- To set up session logging (OPTIONAL), create the table from the console:
29
-
30
- ```ruby
31
- ConsoleAgent.setup!
32
- # => ConsoleAgent: created console_agent_sessions table.
33
- ```
34
-
35
- To reset the table (e.g. after upgrading), run `ConsoleAgent.teardown!` then `ConsoleAgent.setup!`.
36
-
37
- ## Usage
3
+ Claude Code for your Rails Console.
38
4
 
39
5
  ```
40
6
  irb> ai "find the 5 most recent orders over $100"
@@ -50,28 +16,10 @@ Execute? [y/N/edit] y
50
16
  => [#<Order id: 4821, ...>, ...]
51
17
  ```
52
18
 
53
- The AI calls tools behind the scenes to learn your app — schema, models, associations, source code — so it writes accurate queries without you providing any context.
54
-
55
- ### Commands
56
-
57
- | Command | What it does |
58
- |---------|-------------|
59
- | `ai "query"` | One-shot: ask, review code, confirm |
60
- | `ai! "query"` | Interactive: ask and keep chatting |
61
- | `ai? "query"` | Explain only, never executes |
62
-
63
- ### Multi-Step Plans
64
-
65
- For complex tasks, the AI builds a plan and executes it step by step:
19
+ For complex tasks it builds multi-step plans, executing each step sequentially:
66
20
 
67
21
  ```
68
22
  ai> get the most recent salesforce token and count events via the API
69
- Thinking...
70
- -> describe_table("oauth2_tokens")
71
- 28 columns
72
- -> read_file("lib/salesforce_api.rb")
73
- 202 lines
74
-
75
23
  Plan (2 steps):
76
24
  1. Find the most recent active Salesforce OAuth2 token
77
25
  token = Oauth2Token.where(provider: "salesforce", active: true)
@@ -81,155 +29,81 @@ ai> get the most recent salesforce token and count events via the API
81
29
  api.query("SELECT COUNT(Id) FROM Event")
82
30
 
83
31
  Accept plan? [y/N/a(uto)] a
84
- Step 1/2: Find the most recent active Salesforce OAuth2 token
85
- ...
86
- => #<Oauth2Token id: 1, provider: "salesforce", ...>
87
-
88
- Step 2/2: Query event count via SOQL
89
- ...
90
- => [{"expr0"=>42}]
91
- ```
92
-
93
- Each step's return value is available to later steps as `step1`, `step2`, etc.
94
-
95
- Plan prompt options:
96
- - **y** — accept, then confirm each step one at a time
97
- - **a** — accept and auto-run all steps (stays in manual mode for future queries)
98
- - **N** — decline; you're asked what to change and the AI revises
99
-
100
- ### Memories
101
-
102
- The AI remembers what it learns about your codebase across sessions:
103
-
104
- ```
105
- ai> how does sharding work?
106
- -> read_file("config/initializers/sharding.rb")
107
- -> save_memory("Sharding architecture")
108
- Memory saved
109
-
110
- This app uses database-per-shard. User.count returns the current shard only.
111
- ```
112
-
113
- Next time, it already knows — no re-reading files, fewer tokens.
114
-
115
- ### Interactive Mode
116
-
117
- ```
118
- irb> ai!
119
- ConsoleAgent interactive mode. Type 'exit' to leave.
120
- Auto-execute: OFF (Shift-Tab or /auto to toggle) | > code to run directly | /usage | /name <label>
121
-
122
- ai> show me all tables
123
- ...
124
- ai> count orders by status
125
- ...
126
- ai> /auto
127
- Auto-execute: ON
128
- ai> delete cancelled orders older than 90 days
129
- ...
130
- ai> exit
131
32
  ```
132
33
 
133
- Toggle `/auto` to skip confirmation prompts. `/debug` shows raw API traffic. `/usage` shows token stats.
134
-
135
- ### Direct Code Execution
34
+ No context needed from you it figures out your app on its own.
136
35
 
137
- Prefix any input with `>` to run Ruby code directly — no LLM round-trip. The result is added to the conversation context, so the AI knows what happened:
138
-
139
- ```
140
- ai> >User.count
141
- => 8
142
- ai> how many users do I have?
143
- Thinking...
36
+ ## Install
144
37
 
145
- You have **8 users** in your database, as confirmed by the `User.count` you just ran.
38
+ ```ruby
39
+ # Gemfile
40
+ gem 'console_agent', group: :development
146
41
  ```
147
42
 
148
- Useful for quick checks, setting up variables, or giving the AI concrete data to work with.
149
-
150
- ### Sessions
151
-
152
- Sessions are saved automatically when session logging is enabled. You can name, list, and resume them.
153
-
154
- ```
155
- ai> /name sf_user_123_calendar
156
- Session named: sf_user_123_calendar
157
- ai> exit
158
- Session #42 saved.
159
- Resume with: ai_resume "sf_user_123_calendar"
160
- Left ConsoleAgent interactive mode.
43
+ ```bash
44
+ bundle install
45
+ rails generate console_agent:install
161
46
  ```
162
47
 
163
- List recent sessions:
48
+ Set your API key in the generated initializer or via env var (`ANTHROPIC_API_KEY`):
164
49
 
50
+ ```ruby
51
+ # config/initializers/console_agent.rb
52
+ ConsoleAgent.configure do |config|
53
+ config.api_key = 'sk-ant-...'
54
+ end
165
55
  ```
166
- irb> ai_sessions
167
- [Sessions — showing 3]
168
-
169
- #42 sf_user_123_calendar find user 123 with calendar issues
170
- [interactive] 5m ago 2340 tokens
171
56
 
172
- #41 count all active users
173
- [one_shot] 1h ago 850 tokens
174
-
175
- #40 debug_payments explain payment flow
176
- [interactive] 2h ago 4100 tokens
177
-
178
- Use ai_resume(id_or_name) to resume a session.
179
- ```
57
+ ## Commands
180
58
 
181
- Resume a session by name or ID — previous output is replayed, then you continue where you left off:
59
+ | Command | What it does |
60
+ |---------|-------------|
61
+ | `ai "query"` | Ask, review generated code, confirm execution |
62
+ | `ai!` | Enter interactive mode (multi-turn conversation) |
63
+ | `ai? "query"` | Explain only, no execution |
64
+ | `ai_init` | Generate app guide for better AI context |
65
+ | `ai_setup` | Install session logging table |
66
+ | `ai_sessions` | List recent sessions |
67
+ | `ai_resume` | Resume a session by name or ID |
68
+ | `ai_memories` | Show stored memories |
69
+ | `ai_status` | Show current configuration |
182
70
 
183
- ```
184
- irb> ai_resume "sf_user_123_calendar"
185
- --- Replaying previous session output ---
186
- ai> find user 123 with calendar issues
187
- ...previous output...
188
- --- End of previous output ---
189
-
190
- ConsoleAgent interactive mode (sf_user_123_calendar). Type 'exit' to leave.
191
- ai> now check their calendar sync status
192
- ...
193
- ```
71
+ ### Interactive Mode
194
72
 
195
- Name or rename a session after the fact:
73
+ `ai!` starts a conversation. Slash commands available inside:
196
74
 
197
- ```
198
- irb> ai_name 41, "active_user_count"
199
- Session #41 named: active_user_count
200
- ```
75
+ | Command | What it does |
76
+ |---------|-------------|
77
+ | `/auto` | Toggle auto-execute (skip confirmations) |
78
+ | `/compact` | Compress history into a summary (saves tokens) |
79
+ | `/usage` | Show token stats |
80
+ | `/debug` | Toggle raw API output |
81
+ | `/name <label>` | Name the session for easy resume |
201
82
 
202
- Filter sessions by search term:
83
+ Prefix input with `>` to run Ruby directly (no LLM round-trip). The result is added to conversation context.
203
84
 
204
- ```
205
- irb> ai_sessions 20, search: "salesforce"
206
- ```
85
+ ## Features
207
86
 
208
- If you have an existing `console_agent_sessions` table, run `ConsoleAgent.migrate!` to add the `name` column.
87
+ - **Tool use** AI introspects your schema, models, files, and code to write accurate queries
88
+ - **Multi-step plans** — complex tasks are broken into steps, executed sequentially with `step1`/`step2` references
89
+ - **Memories** — AI saves what it learns about your app across sessions
90
+ - **App guide** — `ai_init` generates a guide injected into every system prompt
91
+ - **Sessions** — name, list, and resume interactive conversations (`ai_setup` to enable)
92
+ - **History compaction** — `/compact` summarizes long conversations to reduce cost and latency
209
93
 
210
94
  ## Configuration
211
95
 
212
- All settings live in `config/initializers/console_agent.rb` and can be changed at runtime:
213
-
214
96
  ```ruby
215
97
  ConsoleAgent.configure do |config|
216
98
  config.provider = :anthropic # or :openai
217
99
  config.auto_execute = false # true to skip confirmations
218
- config.max_tokens = 4096 # max tokens per LLM response
219
- config.max_tool_rounds = 10 # max tool calls per query
220
- config.session_logging = true # log sessions to DB (run ConsoleAgent.setup!)
100
+ config.session_logging = true # requires ai_setup
221
101
  end
222
102
  ```
223
103
 
224
- For the admin UI, mount the engine:
225
-
226
- ```ruby
227
- mount ConsoleAgent::Engine => '/console_agent'
228
- ```
229
-
230
104
  ## Requirements
231
105
 
232
- - Ruby >= 2.5, Rails >= 5.0, Faraday >= 1.0
106
+ Ruby >= 2.5, Rails >= 5.0, Faraday >= 1.0
233
107
 
234
108
  ## License
235
109
 
@@ -130,15 +130,34 @@ module ConsoleAgent
130
130
  nil
131
131
  end
132
132
 
133
+ def ai_setup
134
+ ConsoleAgent.setup!
135
+ end
136
+
137
+ def ai_init
138
+ require 'console_agent/context_builder'
139
+ require 'console_agent/providers/base'
140
+ require 'console_agent/executor'
141
+ require 'console_agent/repl'
142
+
143
+ repl = Repl.new(__console_agent_binding)
144
+ repl.init_guide
145
+ rescue => e
146
+ $stderr.puts "\e[31mConsoleAgent error: #{e.message}\e[0m"
147
+ nil
148
+ end
149
+
133
150
  def ai(query = nil)
134
151
  if query.nil?
135
152
  $stderr.puts "\e[33mUsage: ai \"your question here\"\e[0m"
136
153
  $stderr.puts "\e[33m ai \"query\" - ask + confirm execution\e[0m"
137
154
  $stderr.puts "\e[33m ai! \"query\" - enter interactive mode (or ai! with no args)\e[0m"
138
155
  $stderr.puts "\e[33m ai? \"query\" - explain only, no execution\e[0m"
156
+ $stderr.puts "\e[33m ai_init - generate/update app guide for better AI context\e[0m"
139
157
  $stderr.puts "\e[33m ai_sessions - list recent sessions\e[0m"
140
158
  $stderr.puts "\e[33m ai_resume - resume a session by name or id\e[0m"
141
159
  $stderr.puts "\e[33m ai_name - name a session: ai_name 42, \"my_label\"\e[0m"
160
+ $stderr.puts "\e[33m ai_setup - install session logging table\e[0m"
142
161
  $stderr.puts "\e[33m ai_status - show current configuration\e[0m"
143
162
  $stderr.puts "\e[33m ai_memories - show recent memories (ai_memories(n) for last n)\e[0m"
144
163
  return nil
@@ -235,6 +254,14 @@ module ConsoleAgent
235
254
  end
236
255
 
237
256
  def __console_agent_binding
257
+ # Try Pry first (pry-rails replaces IRB but IRB may still be loaded)
258
+ if defined?(Pry)
259
+ pry_inst = ObjectSpace.each_object(Pry).find { |p|
260
+ p.respond_to?(:binding_stack) && !p.binding_stack.empty?
261
+ } rescue nil
262
+ return pry_inst.current_binding if pry_inst
263
+ end
264
+
238
265
  # Try IRB workspace binding
239
266
  if defined?(IRB) && IRB.respond_to?(:CurrentContext)
240
267
  ctx = IRB.CurrentContext rescue nil
@@ -243,14 +270,6 @@ module ConsoleAgent
243
270
  end
244
271
  end
245
272
 
246
- # Try Pry binding
247
- if defined?(Pry) && respond_to?(:pry_instance, true)
248
- pry_inst = pry_instance rescue nil
249
- if pry_inst && pry_inst.respond_to?(:current_binding)
250
- return pry_inst.current_binding
251
- end
252
- end
253
-
254
273
  # Fallback
255
274
  TOPLEVEL_BINDING
256
275
  end
@@ -15,10 +15,32 @@ module ConsoleAgent
15
15
  parts = []
16
16
  parts << smart_system_instructions
17
17
  parts << environment_context
18
+ parts << guide_context
18
19
  parts << memory_context
19
20
  parts.compact.join("\n\n")
20
21
  end
21
22
 
23
+ def environment_context
24
+ lines = ["## Environment"]
25
+ lines << "- Ruby #{RUBY_VERSION}"
26
+ lines << "- Rails #{Rails.version}" if defined?(Rails) && Rails.respond_to?(:version)
27
+
28
+ if defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
29
+ adapter = ActiveRecord::Base.connection.adapter_name rescue 'unknown'
30
+ lines << "- Database adapter: #{adapter}"
31
+ end
32
+
33
+ if defined?(Bundler)
34
+ key_gems = %w[devise cancancan pundit sidekiq delayed_job resque
35
+ paperclip carrierwave activestorage shrine
36
+ pg mysql2 sqlite3 mongoid]
37
+ loaded = key_gems.select { |g| Gem.loaded_specs.key?(g) }
38
+ lines << "- Key gems: #{loaded.join(', ')}" unless loaded.empty?
39
+ end
40
+
41
+ lines.join("\n")
42
+ end
43
+
22
44
  private
23
45
 
24
46
  def smart_system_instructions
@@ -67,25 +89,14 @@ module ConsoleAgent
67
89
  PROMPT
68
90
  end
69
91
 
70
- def environment_context
71
- lines = ["## Environment"]
72
- lines << "- Ruby #{RUBY_VERSION}"
73
- lines << "- Rails #{Rails.version}" if defined?(Rails) && Rails.respond_to?(:version)
74
-
75
- if defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
76
- adapter = ActiveRecord::Base.connection.adapter_name rescue 'unknown'
77
- lines << "- Database adapter: #{adapter}"
78
- end
79
-
80
- if defined?(Bundler)
81
- key_gems = %w[devise cancancan pundit sidekiq delayed_job resque
82
- paperclip carrierwave activestorage shrine
83
- pg mysql2 sqlite3 mongoid]
84
- loaded = key_gems.select { |g| Gem.loaded_specs.key?(g) }
85
- lines << "- Key gems: #{loaded.join(', ')}" unless loaded.empty?
86
- end
92
+ def guide_context
93
+ content = ConsoleAgent.storage.read(ConsoleAgent::GUIDE_KEY)
94
+ return nil if content.nil? || content.strip.empty?
87
95
 
88
- lines.join("\n")
96
+ "## Application Guide\n\n#{content.strip}"
97
+ rescue => e
98
+ ConsoleAgent.logger.debug("ConsoleAgent: guide context failed: #{e.message}")
99
+ nil
89
100
  end
90
101
 
91
102
  def memory_context
@@ -43,7 +43,7 @@ module ConsoleAgent
43
43
  class Executor
44
44
  CODE_REGEX = /```ruby\s*\n(.*?)```/m
45
45
 
46
- attr_reader :binding_context
46
+ attr_reader :binding_context, :last_error
47
47
  attr_accessor :on_prompt
48
48
 
49
49
  def initialize(binding_context)
@@ -75,6 +75,7 @@ module ConsoleAgent
75
75
  def execute(code)
76
76
  return nil if code.nil? || code.strip.empty?
77
77
 
78
+ @last_error = nil
78
79
  captured_output = StringIO.new
79
80
  old_stdout = $stdout
80
81
  # Tee output: capture it and also print to the real stdout
@@ -89,12 +90,14 @@ module ConsoleAgent
89
90
  result
90
91
  rescue SyntaxError => e
91
92
  $stdout = old_stdout if old_stdout
92
- $stderr.puts colorize("SyntaxError: #{e.message}", :red)
93
+ @last_error = "SyntaxError: #{e.message}"
94
+ $stderr.puts colorize(@last_error, :red)
93
95
  @last_output = nil
94
96
  nil
95
97
  rescue => e
96
98
  $stdout = old_stdout if old_stdout
97
- $stderr.puts colorize("Error: #{e.class}: #{e.message}", :red)
99
+ @last_error = "#{e.class}: #{e.message}"
100
+ $stderr.puts colorize("Error: #{@last_error}", :red)
98
101
  e.backtrace.first(3).each { |line| $stderr.puts colorize(" #{line}", :red) }
99
102
  @last_output = captured_output&.string
100
103
  nil
@@ -15,7 +15,7 @@ module ConsoleAgent
15
15
 
16
16
  # Welcome message
17
17
  if $stdout.respond_to?(:tty?) && $stdout.tty?
18
- $stdout.puts "\e[36m[ConsoleAgent] AI assistant loaded. Try: ai \"show me all tables\"\e[0m"
18
+ $stdout.puts "\e[36m[ConsoleAgent v#{ConsoleAgent::VERSION}] AI assistant loaded. Try: ai \"show me all tables\"\e[0m"
19
19
  end
20
20
 
21
21
  # Pre-build context in background
@@ -18,29 +18,25 @@ module ConsoleAgent
18
18
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
19
  console_capture = StringIO.new
20
20
  exec_result = with_console_capture(console_capture) do
21
- result, _ = send_query(query)
22
- track_usage(result)
23
- code = @executor.display_response(result.text)
24
- display_usage(result)
25
-
26
- exec_result = nil
27
- executed = false
28
- has_code = code && !code.strip.empty?
29
-
30
- if has_code
31
- exec_result = if ConsoleAgent.configuration.auto_execute
32
- @executor.execute(code)
33
- else
34
- @executor.confirm_and_execute(code)
35
- end
36
- executed = !@executor.last_cancelled?
21
+ conversation = [{ role: :user, content: query }]
22
+ exec_result, code, executed = one_shot_round(conversation)
23
+
24
+ # Auto-retry once if execution errored
25
+ if executed && @executor.last_error
26
+ error_msg = "Code execution failed with error: #{@executor.last_error}"
27
+ error_msg = error_msg[0..1000] + '...' if error_msg.length > 1000
28
+ conversation << { role: :assistant, content: @_last_result_text }
29
+ conversation << { role: :user, content: error_msg }
30
+
31
+ $stdout.puts "\e[2m Attempting to fix...\e[0m"
32
+ exec_result, code, executed = one_shot_round(conversation)
37
33
  end
38
34
 
39
35
  @_last_log_attrs = {
40
36
  query: query,
41
- conversation: [{ role: :user, content: query }, { role: :assistant, content: result.text }],
37
+ conversation: conversation,
42
38
  mode: 'one_shot',
43
- code_executed: has_code ? code : nil,
39
+ code_executed: code,
44
40
  code_output: executed ? @executor.last_output : nil,
45
41
  code_result: executed && exec_result ? exec_result.inspect : nil,
46
42
  executed: executed,
@@ -61,6 +57,31 @@ module ConsoleAgent
61
57
  nil
62
58
  end
63
59
 
60
+ # Executes one LLM round: send query, display, optionally execute code.
61
+ # Returns [exec_result, code, executed].
62
+ def one_shot_round(conversation)
63
+ result, _ = send_query(nil, conversation: conversation)
64
+ track_usage(result)
65
+ code = @executor.display_response(result.text)
66
+ display_usage(result)
67
+ @_last_result_text = result.text
68
+
69
+ exec_result = nil
70
+ executed = false
71
+ has_code = code && !code.strip.empty?
72
+
73
+ if has_code
74
+ exec_result = if ConsoleAgent.configuration.auto_execute
75
+ @executor.execute(code)
76
+ else
77
+ @executor.confirm_and_execute(code)
78
+ end
79
+ executed = !@executor.last_cancelled?
80
+ end
81
+
82
+ [exec_result, has_code ? code : nil, executed]
83
+ end
84
+
64
85
  def explain(query)
65
86
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
66
87
  console_capture = StringIO.new
@@ -90,6 +111,62 @@ module ConsoleAgent
90
111
  nil
91
112
  end
92
113
 
114
+ def init_guide
115
+ storage = ConsoleAgent.storage
116
+ existing_guide = begin
117
+ content = storage.read(ConsoleAgent::GUIDE_KEY)
118
+ (content && !content.strip.empty?) ? content.strip : nil
119
+ rescue
120
+ nil
121
+ end
122
+
123
+ if existing_guide
124
+ $stdout.puts "\e[36m Existing guide found (#{existing_guide.length} chars). Will update.\e[0m"
125
+ else
126
+ $stdout.puts "\e[36m No existing guide. Exploring the app...\e[0m"
127
+ end
128
+
129
+ require 'console_agent/tools/registry'
130
+ init_tools = Tools::Registry.new(mode: :init)
131
+ sys_prompt = init_system_prompt(existing_guide)
132
+ messages = [{ role: :user, content: "Explore this Rails application and generate the application guide." }]
133
+
134
+ # Temporarily increase timeout — init conversations are large
135
+ original_timeout = ConsoleAgent.configuration.timeout
136
+ ConsoleAgent.configuration.timeout = [original_timeout, 120].max
137
+
138
+ result, _ = send_query_with_tools(messages, system_prompt: sys_prompt, tools_override: init_tools)
139
+
140
+ guide_text = result.text.to_s.strip
141
+ # Strip markdown code fences if the LLM wrapped the response
142
+ guide_text = guide_text.sub(/\A```(?:markdown)?\s*\n?/, '').sub(/\n?```\s*\z/, '')
143
+ # Strip LLM preamble/thinking before the actual guide content
144
+ guide_text = guide_text.sub(/\A.*?(?=^#\s)/m, '') if guide_text =~ /^#\s/m
145
+
146
+ if guide_text.empty?
147
+ $stdout.puts "\e[33m No guide content generated.\e[0m"
148
+ return nil
149
+ end
150
+
151
+ storage.write(ConsoleAgent::GUIDE_KEY, guide_text)
152
+
153
+ path = storage.respond_to?(:root_path) ? File.join(storage.root_path, ConsoleAgent::GUIDE_KEY) : ConsoleAgent::GUIDE_KEY
154
+ $stdout.puts "\e[32m Guide saved to #{path} (#{guide_text.length} chars)\e[0m"
155
+ display_usage(result)
156
+ nil
157
+ rescue Interrupt
158
+ $stdout.puts "\n\e[33m Interrupted.\e[0m"
159
+ nil
160
+ rescue Providers::ProviderError => e
161
+ $stderr.puts "\e[31mConsoleAgent Error: #{e.message}\e[0m"
162
+ nil
163
+ rescue => e
164
+ $stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
165
+ nil
166
+ ensure
167
+ ConsoleAgent.configuration.timeout = original_timeout if original_timeout
168
+ end
169
+
93
170
  def interactive
94
171
  init_interactive_state
95
172
  interactive_loop
@@ -139,6 +216,7 @@ module ConsoleAgent
139
216
  @last_interactive_output = nil
140
217
  @last_interactive_result = nil
141
218
  @last_interactive_executed = false
219
+ @compact_warned = false
142
220
  end
143
221
 
144
222
  def interactive_loop
@@ -146,7 +224,7 @@ module ConsoleAgent
146
224
  name_display = @interactive_session_name ? " (#{@interactive_session_name})" : ""
147
225
  # Write banner to real stdout (bypass TeeIO) so it doesn't accumulate on resume
148
226
  @interactive_old_stdout.puts "\e[36mConsoleAgent interactive mode#{name_display}. Type 'exit' or 'quit' to leave.\e[0m"
149
- @interactive_old_stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle) | > code to run directly | /usage | /name <label>\e[0m"
227
+ @interactive_old_stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle) | > code to run directly | /usage | /compact | /name <label>\e[0m"
150
228
 
151
229
  # Bind Shift-Tab to insert /auto command and submit
152
230
  if Readline.respond_to?(:parse_and_bind)
@@ -180,6 +258,11 @@ module ConsoleAgent
180
258
  next
181
259
  end
182
260
 
261
+ if input == '/compact'
262
+ compact_history
263
+ next
264
+ end
265
+
183
266
  if input.start_with?('/name')
184
267
  name = input.sub('/name', '').strip
185
268
  if name.empty?
@@ -240,65 +323,24 @@ module ConsoleAgent
240
323
  # Save immediately so the session is visible in the admin UI while the AI thinks
241
324
  log_interactive_turn
242
325
 
243
- begin
244
- result, tool_messages = send_query(input, conversation: @history)
245
- rescue Interrupt
246
- $stdout.puts "\n\e[33m Aborted.\e[0m"
326
+ status = send_and_execute
327
+ if status == :interrupted
247
328
  @history.pop # Remove the user message that never got a response
248
329
  log_interactive_turn
249
330
  next
250
331
  end
251
332
 
252
- track_usage(result)
253
- code = @executor.display_response(result.text)
254
- display_usage(result, show_session: true)
255
-
256
- # Save after response is displayed so viewer shows progress before Execute prompt
257
- log_interactive_turn
258
-
259
- # Add tool call/result messages so the LLM remembers what it learned
260
- @history.concat(tool_messages) if tool_messages && !tool_messages.empty?
261
- @history << { role: :assistant, content: result.text }
262
-
263
- if code && !code.strip.empty?
264
- if ConsoleAgent.configuration.auto_execute
265
- exec_result = @executor.execute(code)
266
- else
267
- exec_result = @executor.confirm_and_execute(code)
268
- end
269
-
270
- unless @executor.last_cancelled?
271
- @last_interactive_code = code
272
- @last_interactive_output = @executor.last_output
273
- @last_interactive_result = exec_result ? exec_result.inspect : nil
274
- @last_interactive_executed = true
275
- end
276
-
277
- if @executor.last_cancelled?
278
- @history << { role: :user, content: "User declined to execute the code." }
279
- else
280
- output_parts = []
281
-
282
- # Capture printed output (puts, print, etc.)
283
- if @executor.last_output && !@executor.last_output.strip.empty?
284
- output_parts << "Output:\n#{@executor.last_output.strip}"
285
- end
286
-
287
- # Capture return value
288
- if exec_result
289
- output_parts << "Return value: #{exec_result.inspect}"
290
- end
291
-
292
- unless output_parts.empty?
293
- result_str = output_parts.join("\n\n")
294
- result_str = result_str[0..1000] + '...' if result_str.length > 1000
295
- @history << { role: :user, content: "Code was executed. #{result_str}" }
296
- end
297
- end
333
+ # Auto-retry once when execution fails — send error back to LLM for a fix
334
+ if status == :error
335
+ $stdout.puts "\e[2m Attempting to fix...\e[0m"
336
+ log_interactive_turn
337
+ send_and_execute
298
338
  end
299
339
 
300
340
  # Update with the AI response, tokens, and any execution results
301
341
  log_interactive_turn
342
+
343
+ warn_if_history_large
302
344
  end
303
345
 
304
346
  $stdout = @interactive_old_stdout
@@ -318,6 +360,73 @@ module ConsoleAgent
318
360
  $stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
319
361
  end
320
362
 
363
+ # Sends conversation to LLM, displays response, executes code if present.
364
+ # Returns :success, :error, :cancelled, :no_code, or :interrupted.
365
+ def send_and_execute
366
+ begin
367
+ result, tool_messages = send_query(nil, conversation: @history)
368
+ rescue Interrupt
369
+ $stdout.puts "\n\e[33m Aborted.\e[0m"
370
+ return :interrupted
371
+ end
372
+
373
+ track_usage(result)
374
+ code = @executor.display_response(result.text)
375
+ display_usage(result, show_session: true)
376
+
377
+ # Save after response is displayed so viewer shows progress before Execute prompt
378
+ log_interactive_turn
379
+
380
+ # Add tool call/result messages so the LLM remembers what it learned
381
+ @history.concat(tool_messages) if tool_messages && !tool_messages.empty?
382
+ @history << { role: :assistant, content: result.text }
383
+
384
+ return :no_code unless code && !code.strip.empty?
385
+
386
+ exec_result = if ConsoleAgent.configuration.auto_execute
387
+ @executor.execute(code)
388
+ else
389
+ @executor.confirm_and_execute(code)
390
+ end
391
+
392
+ unless @executor.last_cancelled?
393
+ @last_interactive_code = code
394
+ @last_interactive_output = @executor.last_output
395
+ @last_interactive_result = exec_result ? exec_result.inspect : nil
396
+ @last_interactive_executed = true
397
+ end
398
+
399
+ if @executor.last_cancelled?
400
+ @history << { role: :user, content: "User declined to execute the code." }
401
+ :cancelled
402
+ elsif @executor.last_error
403
+ error_msg = "Code execution failed with error: #{@executor.last_error}"
404
+ error_msg = error_msg[0..1000] + '...' if error_msg.length > 1000
405
+ @history << { role: :user, content: error_msg }
406
+ :error
407
+ else
408
+ output_parts = []
409
+
410
+ # Capture printed output (puts, print, etc.)
411
+ if @executor.last_output && !@executor.last_output.strip.empty?
412
+ output_parts << "Output:\n#{@executor.last_output.strip}"
413
+ end
414
+
415
+ # Capture return value
416
+ if exec_result
417
+ output_parts << "Return value: #{exec_result.inspect}"
418
+ end
419
+
420
+ unless output_parts.empty?
421
+ result_str = output_parts.join("\n\n")
422
+ result_str = result_str[0..1000] + '...' if result_str.length > 1000
423
+ @history << { role: :user, content: "Code was executed. #{result_str}" }
424
+ end
425
+
426
+ :success
427
+ end
428
+ end
429
+
321
430
  def provider
322
431
  @provider ||= Providers.build
323
432
  end
@@ -327,7 +436,76 @@ module ConsoleAgent
327
436
  end
328
437
 
329
438
  def context
330
- @context ||= context_builder.build
439
+ base = @context_base ||= context_builder.build
440
+ vars = binding_variable_summary
441
+ vars ? "#{base}\n\n#{vars}" : base
442
+ end
443
+
444
+ # Summarize local and instance variables from the user's console session
445
+ # so the LLM knows what's available to reference in generated code.
446
+ def binding_variable_summary
447
+ parts = []
448
+
449
+ locals = @binding_context.local_variables.reject { |v| v.to_s.start_with?('_') }
450
+ locals.first(20).each do |var|
451
+ val = @binding_context.local_variable_get(var) rescue nil
452
+ parts << "#{var} (#{val.class})"
453
+ end
454
+
455
+ ivars = (@binding_context.eval("instance_variables") rescue [])
456
+ ivars.reject { |v| v.to_s =~ /\A@_/ }.first(20).each do |var|
457
+ val = @binding_context.eval(var.to_s) rescue nil
458
+ parts << "#{var} (#{val.class})"
459
+ end
460
+
461
+ return nil if parts.empty?
462
+ "The user's console session has these variables available: #{parts.join(', ')}. You can reference them directly in code."
463
+ rescue
464
+ nil
465
+ end
466
+
467
+ def init_system_prompt(existing_guide)
468
+ env = context_builder.environment_context
469
+
470
+ prompt = <<~PROMPT
471
+ You are a Rails application analyst. Your job is to explore this Rails app using the
472
+ available tools and produce a concise markdown guide that will be injected into future
473
+ AI assistant sessions.
474
+
475
+ #{env}
476
+
477
+ EXPLORATION STRATEGY — be efficient to avoid timeouts:
478
+ 1. Start with list_models to see all models and their associations
479
+ 2. Pick the 5-8 CORE models and call describe_model on those only
480
+ 3. Call describe_table on only 3-5 key tables (skip tables whose models already told you enough)
481
+ 4. Use search_code sparingly — only for specific patterns you suspect (sharding, STI, concerns)
482
+ 5. Use read_file only when you need to understand a specific pattern (read small sections, not whole files)
483
+ 6. Do NOT exhaustively describe every table or model — focus on what's important
484
+
485
+ IMPORTANT: Keep your total tool calls under 20. Prioritize breadth over depth.
486
+
487
+ Produce a markdown document with these sections:
488
+ - **Application Overview**: What the app does, key domain concepts
489
+ - **Key Models & Relationships**: Core models and how they relate
490
+ - **Data Architecture**: Important tables, notable columns, any partitioning/sharding
491
+ - **Important Patterns**: Custom concerns, service objects, key abstractions
492
+ - **Common Maintenance Tasks**: Typical console operations for this app
493
+ - **Gotchas**: Non-obvious behaviors, tricky associations, known quirks
494
+
495
+ Keep it concise — aim for 1-2 pages. Focus on what a console user needs to know.
496
+ Do NOT wrap the output in markdown code fences.
497
+ PROMPT
498
+
499
+ if existing_guide
500
+ prompt += <<~UPDATE
501
+
502
+ Here is the existing guide. Update and merge with any new findings:
503
+
504
+ #{existing_guide}
505
+ UPDATE
506
+ end
507
+
508
+ prompt.strip
331
509
  end
332
510
 
333
511
  def send_query(query, conversation: nil)
@@ -342,45 +520,43 @@ module ConsoleAgent
342
520
  send_query_with_tools(messages)
343
521
  end
344
522
 
345
- def send_query_with_tools(messages)
523
+ def send_query_with_tools(messages, system_prompt: nil, tools_override: nil)
346
524
  require 'console_agent/tools/registry'
347
- tools = Tools::Registry.new(executor: @executor)
525
+ tools = tools_override || Tools::Registry.new(executor: @executor)
526
+ active_system_prompt = system_prompt || context
348
527
  max_rounds = ConsoleAgent.configuration.max_tool_rounds
349
528
  total_input = 0
350
529
  total_output = 0
351
530
  result = nil
352
531
  new_messages = [] # Track messages added during tool use
532
+ last_thinking = nil
533
+ last_tool_names = []
353
534
 
354
535
  exhausted = false
355
536
 
356
537
  max_rounds.times do |round|
357
538
  if round == 0
358
539
  $stdout.puts "\e[2m Thinking...\e[0m"
540
+ else
541
+ # Show buffered thinking text before the "Calling LLM" line
542
+ if last_thinking
543
+ last_thinking.split("\n").each do |line|
544
+ $stdout.puts "\e[2m #{line}\e[0m"
545
+ end
546
+ end
547
+ $stdout.puts "\e[2m #{llm_status(round, messages, total_input, last_thinking, last_tool_names)}\e[0m"
359
548
  end
360
549
 
361
- begin
362
- result = with_escape_monitoring do
363
- provider.chat_with_tools(messages, tools: tools, system_prompt: context)
364
- end
365
- rescue Interrupt
366
- redirect = prompt_for_redirect
367
- if redirect
368
- messages << { role: :user, content: redirect }
369
- new_messages << messages.last
370
- next
371
- else
372
- raise
373
- end
550
+ result = with_escape_monitoring do
551
+ provider.chat_with_tools(messages, tools: tools, system_prompt: active_system_prompt)
374
552
  end
375
553
  total_input += result.input_tokens || 0
376
554
  total_output += result.output_tokens || 0
377
555
 
378
556
  break unless result.tool_use?
379
557
 
380
- # Show what the LLM is thinking (if it returned text alongside tool calls)
381
- if result.text && !result.text.strip.empty?
382
- $stdout.puts "\e[2m #{result.text.strip}\e[0m"
383
- end
558
+ # Buffer thinking text for display before next LLM call
559
+ last_thinking = (result.text && !result.text.strip.empty?) ? result.text.strip : nil
384
560
 
385
561
  # Add assistant message with tool calls to conversation
386
562
  assistant_msg = provider.format_assistant_message(result)
@@ -388,6 +564,7 @@ module ConsoleAgent
388
564
  new_messages << assistant_msg
389
565
 
390
566
  # Execute each tool and show progress
567
+ last_tool_names = result.tool_calls.map { |tc| tc[:name] }
391
568
  result.tool_calls.each do |tc|
392
569
  # ask_user and execute_plan handle their own display
393
570
  if tc[:name] == 'ask_user' || tc[:name] == 'execute_plan'
@@ -419,7 +596,7 @@ module ConsoleAgent
419
596
  if exhausted
420
597
  $stdout.puts "\e[33m Hit tool round limit (#{max_rounds}). Forcing final answer. Increase with: ConsoleAgent.configure { |c| c.max_tool_rounds = 200 }\e[0m"
421
598
  messages << { role: :user, content: "You've used all available tool rounds. Please provide your best answer now based on what you've learned so far." }
422
- result = provider.chat(messages, system_prompt: context)
599
+ result = provider.chat(messages, system_prompt: active_system_prompt)
423
600
  total_input += result.input_tokens || 0
424
601
  total_output += result.output_tokens || 0
425
602
  end
@@ -477,13 +654,29 @@ module ConsoleAgent
477
654
  end
478
655
  end
479
656
 
480
- def prompt_for_redirect
481
- $stdout.puts "\n\e[33m Interrupted. What should the AI do differently?\e[0m"
482
- $stdout.puts "\e[2m (Press Enter with no input to abort entirely)\e[0m"
483
- $stdout.print "\e[33m redirect> \e[0m"
484
- input = $stdin.gets
485
- return nil if input.nil? || input.strip.empty?
486
- input.strip
657
+
658
+ def llm_status(round, messages, tokens_so_far, last_thinking = nil, last_tool_names = [])
659
+ status = "Calling LLM (round #{round + 1}, #{messages.length} msgs"
660
+ status += ", ~#{format_tokens(tokens_so_far)} ctx" if tokens_so_far > 0
661
+ status += ")"
662
+ if !last_thinking && last_tool_names.any?
663
+ # Summarize tools when there's no thinking text
664
+ counts = last_tool_names.tally
665
+ summary = counts.map { |name, n| n > 1 ? "#{name} x#{n}" : name }.join(", ")
666
+ status += " after #{summary}"
667
+ end
668
+ status += "..."
669
+ status
670
+ end
671
+
672
+ def format_tokens(count)
673
+ if count >= 1_000_000
674
+ "#{(count / 1_000_000.0).round(1)}M"
675
+ elsif count >= 1_000
676
+ "#{(count / 1_000.0).round(1)}K"
677
+ else
678
+ count.to_s
679
+ end
487
680
  end
488
681
 
489
682
  def format_tool_args(name, args)
@@ -547,8 +740,12 @@ module ConsoleAgent
547
740
  lines = result.split("\n")
548
741
  "#{lines.length} files"
549
742
  when 'read_file'
550
- lines = result.split("\n")
551
- "#{lines.length} lines"
743
+ if result =~ /^Lines (\d+)-(\d+) of (\d+):/
744
+ "lines #{$1}-#{$2} of #{$3}"
745
+ else
746
+ lines = result.split("\n")
747
+ "#{lines.length} lines"
748
+ end
552
749
  when 'search_code'
553
750
  if result.start_with?('Found')
554
751
  result.split("\n").first
@@ -686,6 +883,64 @@ module ConsoleAgent
686
883
  $stdout.puts "\e[2m[session totals — in: #{@total_input_tokens} | out: #{@total_output_tokens} | total: #{@total_input_tokens + @total_output_tokens}]\e[0m"
687
884
  end
688
885
 
886
+ def warn_if_history_large
887
+ chars = @history.sum { |m| m[:content].to_s.length }
888
+ return if chars < 50_000 || @compact_warned
889
+
890
+ @compact_warned = true
891
+ $stdout.puts "\e[33m Conversation is getting large (~#{format_tokens(chars)} chars). Consider running /compact to reduce context size.\e[0m"
892
+ end
893
+
894
+ def compact_history
895
+ if @history.length < 6
896
+ $stdout.puts "\e[33m History too short to compact (#{@history.length} messages). Need at least 6.\e[0m"
897
+ return
898
+ end
899
+
900
+ before_chars = @history.sum { |m| m[:content].to_s.length }
901
+ before_count = @history.length
902
+
903
+ $stdout.puts "\e[2m Compacting #{before_count} messages (~#{format_tokens(before_chars)} chars)...\e[0m"
904
+
905
+ system_prompt = <<~PROMPT
906
+ You are a conversation summarizer. The user will provide a conversation history from a Rails console AI assistant session.
907
+
908
+ Produce a concise summary that captures:
909
+ - What the user has been working on and their goals
910
+ - Key findings and data discovered (include specific values, IDs, record counts)
911
+ - Current state: what worked, what failed, where things stand
912
+ - Important variable names, model names, or table names referenced
913
+ - Any code that was executed and its results
914
+
915
+ Be concise but preserve all information that would be needed to continue the conversation naturally.
916
+ Do NOT include any preamble — just output the summary directly.
917
+ PROMPT
918
+
919
+ history_text = @history.map { |m| "#{m[:role]}: #{m[:content]}" }.join("\n\n")
920
+ messages = [{ role: :user, content: "Summarize this conversation history:\n\n#{history_text}" }]
921
+
922
+ begin
923
+ result = provider.chat(messages, system_prompt: system_prompt)
924
+ track_usage(result)
925
+
926
+ summary = result.text.to_s.strip
927
+ if summary.empty?
928
+ $stdout.puts "\e[33m Compaction failed: empty summary returned.\e[0m"
929
+ return
930
+ end
931
+
932
+ @history = [{ role: :user, content: "CONVERSATION SUMMARY (compacted):\n#{summary}" }]
933
+ @compact_warned = false
934
+
935
+ after_chars = @history.first[:content].length
936
+ $stdout.puts "\e[36m Compacted: #{before_count} messages -> 1 summary (~#{format_tokens(before_chars)} -> ~#{format_tokens(after_chars)} chars)\e[0m"
937
+ summary.each_line { |line| $stdout.puts "\e[2m #{line.rstrip}\e[0m" }
938
+ display_usage(result)
939
+ rescue => e
940
+ $stdout.puts "\e[31m Compaction failed: #{e.message}\e[0m"
941
+ end
942
+ end
943
+
689
944
  def display_exit_info
690
945
  display_session_summary
691
946
  if @interactive_session_id
@@ -1,7 +1,7 @@
1
1
  module ConsoleAgent
2
2
  module Tools
3
3
  class CodeTools
4
- MAX_FILE_LINES = 200
4
+ MAX_FILE_LINES = 500
5
5
  MAX_LIST_ENTRIES = 100
6
6
  MAX_SEARCH_RESULTS = 50
7
7
 
@@ -28,7 +28,7 @@ module ConsoleAgent
28
28
  "Error listing files: #{e.message}"
29
29
  end
30
30
 
31
- def read_file(path)
31
+ def read_file(path, start_line: nil, end_line: nil)
32
32
  return "Error: path is required." if path.nil? || path.strip.empty?
33
33
 
34
34
  root = rails_root
@@ -45,12 +45,24 @@ module ConsoleAgent
45
45
  return "File '#{path}' not found." unless File.exist?(full_path)
46
46
  return "Error: '#{path}' is a directory, not a file." if File.directory?(full_path)
47
47
 
48
- lines = File.readlines(full_path)
49
- if lines.length > MAX_FILE_LINES
50
- numbered = lines.first(MAX_FILE_LINES).each_with_index.map { |line, i| "#{i + 1}: #{line}" }
51
- numbered.join + "\n... truncated (#{lines.length} total lines, showing first #{MAX_FILE_LINES})"
48
+ all_lines = File.readlines(full_path)
49
+ total = all_lines.length
50
+
51
+ # Apply line range if specified (1-based, inclusive)
52
+ if start_line || end_line
53
+ s = [(start_line || 1).to_i, 1].max
54
+ e = [(end_line || total).to_i, total].min
55
+ return "Error: start_line (#{s}) is beyond end of file (#{total} lines)." if s > total
56
+ lines = all_lines[(s - 1)..(e - 1)] || []
57
+ offset = s - 1
58
+ numbered = lines.each_with_index.map { |line, i| "#{offset + i + 1}: #{line}" }
59
+ header = "Lines #{s}-#{[e, s + lines.length - 1].min} of #{total}:\n"
60
+ header + numbered.join
61
+ elsif total > MAX_FILE_LINES
62
+ numbered = all_lines.first(MAX_FILE_LINES).each_with_index.map { |line, i| "#{i + 1}: #{line}" }
63
+ numbered.join + "\n... truncated (#{total} total lines, showing first #{MAX_FILE_LINES}). Use start_line/end_line to read specific sections."
52
64
  else
53
- lines.each_with_index.map { |line, i| "#{i + 1}: #{line}" }.join
65
+ all_lines.each_with_index.map { |line, i| "#{i + 1}: #{line}" }.join
54
66
  end
55
67
  rescue => e
56
68
  "Error reading file '#{path}': #{e.message}"
@@ -8,8 +8,9 @@ module ConsoleAgent
8
8
  # Tools that should never be cached (side effects or user interaction)
9
9
  NO_CACHE = %w[ask_user save_memory delete_memory execute_plan].freeze
10
10
 
11
- def initialize(executor: nil)
11
+ def initialize(executor: nil, mode: :default)
12
12
  @executor = executor
13
+ @mode = mode
13
14
  @definitions = []
14
15
  @handlers = {}
15
16
  @cache = {}
@@ -142,15 +143,17 @@ module ConsoleAgent
142
143
 
143
144
  register(
144
145
  name: 'read_file',
145
- description: 'Read the contents of a file in this Rails app. Capped at 200 lines.',
146
+ description: 'Read the contents of a file in this Rails app. Returns up to 500 lines by default. Use start_line/end_line to read specific sections of large files.',
146
147
  parameters: {
147
148
  'type' => 'object',
148
149
  'properties' => {
149
- 'path' => { 'type' => 'string', 'description' => 'Relative file path (e.g. "app/models/user.rb")' }
150
+ 'path' => { 'type' => 'string', 'description' => 'Relative file path (e.g. "app/models/user.rb")' },
151
+ 'start_line' => { 'type' => 'integer', 'description' => 'First line to read (1-based). Optional — omit to start from beginning.' },
152
+ 'end_line' => { 'type' => 'integer', 'description' => 'Last line to read (1-based, inclusive). Optional — omit to read to end.' }
150
153
  },
151
154
  'required' => ['path']
152
155
  },
153
- handler: ->(args) { code.read_file(args['path']) }
156
+ handler: ->(args) { code.read_file(args['path'], start_line: args['start_line'], end_line: args['end_line']) }
154
157
  )
155
158
 
156
159
  register(
@@ -167,21 +170,23 @@ module ConsoleAgent
167
170
  handler: ->(args) { code.search_code(args['query'], args['directory']) }
168
171
  )
169
172
 
170
- register(
171
- name: 'ask_user',
172
- description: 'Ask the console user a clarifying question. Use this when you need specific information to write accurate code (e.g. which user they are, which record to target, what value to use). Do NOT generate placeholder values like YOUR_USER_ID — ask instead.',
173
- parameters: {
174
- 'type' => 'object',
175
- 'properties' => {
176
- 'question' => { 'type' => 'string', 'description' => 'The question to ask the user' }
173
+ unless @mode == :init
174
+ register(
175
+ name: 'ask_user',
176
+ description: 'Ask the console user a clarifying question. Use this when you need specific information to write accurate code (e.g. which user they are, which record to target, what value to use). Do NOT generate placeholder values like YOUR_USER_ID — ask instead.',
177
+ parameters: {
178
+ 'type' => 'object',
179
+ 'properties' => {
180
+ 'question' => { 'type' => 'string', 'description' => 'The question to ask the user' }
181
+ },
182
+ 'required' => ['question']
177
183
  },
178
- 'required' => ['question']
179
- },
180
- handler: ->(args) { ask_user(args['question']) }
181
- )
184
+ handler: ->(args) { ask_user(args['question']) }
185
+ )
182
186
 
183
- register_memory_tools
184
- register_execute_plan
187
+ register_memory_tools
188
+ register_execute_plan
189
+ end
185
190
  end
186
191
 
187
192
  def register_memory_tools
@@ -334,8 +339,12 @@ module ConsoleAgent
334
339
  # Make result available as step1, step2, etc. for subsequent steps
335
340
  @executor.binding_context.local_variable_set(:"step#{i + 1}", exec_result)
336
341
  output = @executor.last_output
342
+ error = @executor.last_error
337
343
 
338
344
  step_report = "Step #{i + 1} (#{step['description']}):\n"
345
+ if error
346
+ step_report += "ERROR: #{error}\n"
347
+ end
339
348
  if output && !output.strip.empty?
340
349
  step_report += "Output: #{output.strip}\n"
341
350
  end
@@ -1,3 +1,3 @@
1
1
  module ConsoleAgent
2
- VERSION = '0.5.0'.freeze
2
+ VERSION = '0.7.0'.freeze
3
3
  end
data/lib/console_agent.rb CHANGED
@@ -2,6 +2,8 @@ require 'console_agent/version'
2
2
  require 'console_agent/configuration'
3
3
 
4
4
  module ConsoleAgent
5
+ GUIDE_KEY = 'console_agent.md'.freeze
6
+
5
7
  class << self
6
8
  def configuration
7
9
  @configuration ||= Configuration.new
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: console_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cortfr