console_agent 0.6.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: d1d37fc84ac3f2d29832d282e6db1fd394308de2b967843c3a0c9748560a7025
4
- data.tar.gz: 28bd6c25529dbedfa12d2353a1648304517bab933f0a4f25e91285ccaaf08e10
3
+ metadata.gz: 662ece5e5732e350b22871c8029421edf06ed4d9f98183f5cee7e95a3c1099f6
4
+ data.tar.gz: 483642598de39d23ace26f5d90863a8712b8799357c7809b2ba770f2bdc2522a
5
5
  SHA512:
6
- metadata.gz: 6451c8b62eba1159e0233be04dabbc86bcb3b6f1dcbd6cd658c8d5517a27f1ad6a20884d30d87fabcbe75d8a51d12a6dfa2d4a59ed0adb648ce56d3b81765636
7
- data.tar.gz: 3448b25930e4613a4599698d98ec1c5a2d84b113a3221d98239dc0f09351455be817882b308910bfd8bf01eab00d01670c11256624bb7cd7a30745538095c2ea
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,29 +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
- | `ai_init` | Generate/update app guide for better context |
63
-
64
- ### Multi-Step Plans
65
-
66
- 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:
67
20
 
68
21
  ```
69
22
  ai> get the most recent salesforce token and count events via the API
70
- Thinking...
71
- -> describe_table("oauth2_tokens")
72
- 28 columns
73
- -> read_file("lib/salesforce_api.rb")
74
- 202 lines
75
-
76
23
  Plan (2 steps):
77
24
  1. Find the most recent active Salesforce OAuth2 token
78
25
  token = Oauth2Token.where(provider: "salesforce", active: true)
@@ -82,179 +29,81 @@ ai> get the most recent salesforce token and count events via the API
82
29
  api.query("SELECT COUNT(Id) FROM Event")
83
30
 
84
31
  Accept plan? [y/N/a(uto)] a
85
- Step 1/2: Find the most recent active Salesforce OAuth2 token
86
- ...
87
- => #<Oauth2Token id: 1, provider: "salesforce", ...>
88
-
89
- Step 2/2: Query event count via SOQL
90
- ...
91
- => [{"expr0"=>42}]
92
32
  ```
93
33
 
94
- Each step's return value is available to later steps as `step1`, `step2`, etc.
95
-
96
- Plan prompt options:
97
- - **y** — accept, then confirm each step one at a time
98
- - **a** — accept and auto-run all steps (stays in manual mode for future queries)
99
- - **N** — decline; you're asked what to change and the AI revises
34
+ No context needed from you it figures out your app on its own.
100
35
 
101
- ### Memories
102
-
103
- The AI remembers what it learns about your codebase across sessions:
36
+ ## Install
104
37
 
38
+ ```ruby
39
+ # Gemfile
40
+ gem 'console_agent', group: :development
105
41
  ```
106
- ai> how does sharding work?
107
- -> read_file("config/initializers/sharding.rb")
108
- -> save_memory("Sharding architecture")
109
- Memory saved
110
42
 
111
- This app uses database-per-shard. User.count returns the current shard only.
43
+ ```bash
44
+ bundle install
45
+ rails generate console_agent:install
112
46
  ```
113
47
 
114
- Next time, it already knows no re-reading files, fewer tokens.
115
-
116
- ### Application Guide
48
+ Set your API key in the generated initializer or via env var (`ANTHROPIC_API_KEY`):
117
49
 
118
- Run `ai_init` to have the AI explore your app and generate a guide that gets loaded into every future conversation:
119
-
120
- ```
121
- irb> ai_init
122
- No existing guide. Exploring the app...
123
- Thinking...
124
- -> list_models
125
- 240 models
126
- -> describe_model("User")
127
- 119 associations, 6 validations
128
- -> describe_model("Account")
129
- 25 associations
130
- -> search_code("Sharding", dir: "config")
131
- Found 36 matches
132
- ...
133
- Guide saved to .console_agent/console_agent.md (3204 chars)
50
+ ```ruby
51
+ # config/initializers/console_agent.rb
52
+ ConsoleAgent.configure do |config|
53
+ config.api_key = 'sk-ant-...'
54
+ end
134
55
  ```
135
56
 
136
- The guide is a markdown file covering your app's models, relationships, data architecture, and gotchas. Unlike memories (which require a tool call to recall), the guide is injected directly into the system prompt — so the AI starts every session already knowing your app.
57
+ ## Commands
137
58
 
138
- Run `ai_init` again anytime to update it.
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 |
139
70
 
140
71
  ### Interactive Mode
141
72
 
142
- ```
143
- irb> ai!
144
- ConsoleAgent interactive mode. Type 'exit' to leave.
145
- Auto-execute: OFF (Shift-Tab or /auto to toggle) | > code to run directly | /usage | /name <label>
146
-
147
- ai> show me all tables
148
- ...
149
- ai> count orders by status
150
- ...
151
- ai> /auto
152
- Auto-execute: ON
153
- ai> delete cancelled orders older than 90 days
154
- ...
155
- ai> exit
156
- ```
157
-
158
- Toggle `/auto` to skip confirmation prompts. `/debug` shows raw API traffic. `/usage` shows token stats.
159
-
160
- ### Direct Code Execution
161
-
162
- 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:
163
-
164
- ```
165
- ai> >User.count
166
- => 8
167
- ai> how many users do I have?
168
- Thinking...
169
-
170
- You have **8 users** in your database, as confirmed by the `User.count` you just ran.
171
- ```
172
-
173
- Useful for quick checks, setting up variables, or giving the AI concrete data to work with.
174
-
175
- ### Sessions
176
-
177
- Sessions are saved automatically when session logging is enabled. You can name, list, and resume them.
178
-
179
- ```
180
- ai> /name sf_user_123_calendar
181
- Session named: sf_user_123_calendar
182
- ai> exit
183
- Session #42 saved.
184
- Resume with: ai_resume "sf_user_123_calendar"
185
- Left ConsoleAgent interactive mode.
186
- ```
187
-
188
- List recent sessions:
73
+ `ai!` starts a conversation. Slash commands available inside:
189
74
 
190
- ```
191
- irb> ai_sessions
192
- [Sessions showing 3]
193
-
194
- #42 sf_user_123_calendar find user 123 with calendar issues
195
- [interactive] 5m ago 2340 tokens
196
-
197
- #41 count all active users
198
- [one_shot] 1h ago 850 tokens
199
-
200
- #40 debug_payments explain payment flow
201
- [interactive] 2h ago 4100 tokens
202
-
203
- Use ai_resume(id_or_name) to resume a session.
204
- ```
205
-
206
- Resume a session by name or ID — previous output is replayed, then you continue where you left off:
207
-
208
- ```
209
- irb> ai_resume "sf_user_123_calendar"
210
- --- Replaying previous session output ---
211
- ai> find user 123 with calendar issues
212
- ...previous output...
213
- --- End of previous output ---
214
-
215
- ConsoleAgent interactive mode (sf_user_123_calendar). Type 'exit' to leave.
216
- ai> now check their calendar sync status
217
- ...
218
- ```
219
-
220
- Name or rename a session after the fact:
221
-
222
- ```
223
- irb> ai_name 41, "active_user_count"
224
- Session #41 named: active_user_count
225
- ```
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 |
226
82
 
227
- Filter sessions by search term:
83
+ Prefix input with `>` to run Ruby directly (no LLM round-trip). The result is added to conversation context.
228
84
 
229
- ```
230
- irb> ai_sessions 20, search: "salesforce"
231
- ```
85
+ ## Features
232
86
 
233
- 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
234
93
 
235
94
  ## Configuration
236
95
 
237
- All settings live in `config/initializers/console_agent.rb` and can be changed at runtime:
238
-
239
96
  ```ruby
240
97
  ConsoleAgent.configure do |config|
241
98
  config.provider = :anthropic # or :openai
242
99
  config.auto_execute = false # true to skip confirmations
243
- config.max_tokens = 4096 # max tokens per LLM response
244
- config.max_tool_rounds = 10 # max tool calls per query
245
- config.session_logging = true # log sessions to DB (run ConsoleAgent.setup!)
100
+ config.session_logging = true # requires ai_setup
246
101
  end
247
102
  ```
248
103
 
249
- For the admin UI, mount the engine:
250
-
251
- ```ruby
252
- mount ConsoleAgent::Engine => '/console_agent'
253
- ```
254
-
255
104
  ## Requirements
256
105
 
257
- - Ruby >= 2.5, Rails >= 5.0, Faraday >= 1.0
106
+ Ruby >= 2.5, Rails >= 5.0, Faraday >= 1.0
258
107
 
259
108
  ## License
260
109
 
@@ -130,6 +130,10 @@ module ConsoleAgent
130
130
  nil
131
131
  end
132
132
 
133
+ def ai_setup
134
+ ConsoleAgent.setup!
135
+ end
136
+
133
137
  def ai_init
134
138
  require 'console_agent/context_builder'
135
139
  require 'console_agent/providers/base'
@@ -153,6 +157,7 @@ module ConsoleAgent
153
157
  $stderr.puts "\e[33m ai_sessions - list recent sessions\e[0m"
154
158
  $stderr.puts "\e[33m ai_resume - resume a session by name or id\e[0m"
155
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"
156
161
  $stderr.puts "\e[33m ai_status - show current configuration\e[0m"
157
162
  $stderr.puts "\e[33m ai_memories - show recent memories (ai_memories(n) for last n)\e[0m"
158
163
  return nil
@@ -249,6 +254,14 @@ module ConsoleAgent
249
254
  end
250
255
 
251
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
+
252
265
  # Try IRB workspace binding
253
266
  if defined?(IRB) && IRB.respond_to?(:CurrentContext)
254
267
  ctx = IRB.CurrentContext rescue nil
@@ -257,14 +270,6 @@ module ConsoleAgent
257
270
  end
258
271
  end
259
272
 
260
- # Try Pry binding
261
- if defined?(Pry) && respond_to?(:pry_instance, true)
262
- pry_inst = pry_instance rescue nil
263
- if pry_inst && pry_inst.respond_to?(:current_binding)
264
- return pry_inst.current_binding
265
- end
266
- end
267
-
268
273
  # Fallback
269
274
  TOPLEVEL_BINDING
270
275
  end
@@ -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
@@ -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
@@ -195,6 +216,7 @@ module ConsoleAgent
195
216
  @last_interactive_output = nil
196
217
  @last_interactive_result = nil
197
218
  @last_interactive_executed = false
219
+ @compact_warned = false
198
220
  end
199
221
 
200
222
  def interactive_loop
@@ -202,7 +224,7 @@ module ConsoleAgent
202
224
  name_display = @interactive_session_name ? " (#{@interactive_session_name})" : ""
203
225
  # Write banner to real stdout (bypass TeeIO) so it doesn't accumulate on resume
204
226
  @interactive_old_stdout.puts "\e[36mConsoleAgent interactive mode#{name_display}. Type 'exit' or 'quit' to leave.\e[0m"
205
- @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"
206
228
 
207
229
  # Bind Shift-Tab to insert /auto command and submit
208
230
  if Readline.respond_to?(:parse_and_bind)
@@ -236,6 +258,11 @@ module ConsoleAgent
236
258
  next
237
259
  end
238
260
 
261
+ if input == '/compact'
262
+ compact_history
263
+ next
264
+ end
265
+
239
266
  if input.start_with?('/name')
240
267
  name = input.sub('/name', '').strip
241
268
  if name.empty?
@@ -296,65 +323,24 @@ module ConsoleAgent
296
323
  # Save immediately so the session is visible in the admin UI while the AI thinks
297
324
  log_interactive_turn
298
325
 
299
- begin
300
- result, tool_messages = send_query(input, conversation: @history)
301
- rescue Interrupt
302
- $stdout.puts "\n\e[33m Aborted.\e[0m"
326
+ status = send_and_execute
327
+ if status == :interrupted
303
328
  @history.pop # Remove the user message that never got a response
304
329
  log_interactive_turn
305
330
  next
306
331
  end
307
332
 
308
- track_usage(result)
309
- code = @executor.display_response(result.text)
310
- display_usage(result, show_session: true)
311
-
312
- # Save after response is displayed so viewer shows progress before Execute prompt
313
- log_interactive_turn
314
-
315
- # Add tool call/result messages so the LLM remembers what it learned
316
- @history.concat(tool_messages) if tool_messages && !tool_messages.empty?
317
- @history << { role: :assistant, content: result.text }
318
-
319
- if code && !code.strip.empty?
320
- if ConsoleAgent.configuration.auto_execute
321
- exec_result = @executor.execute(code)
322
- else
323
- exec_result = @executor.confirm_and_execute(code)
324
- end
325
-
326
- unless @executor.last_cancelled?
327
- @last_interactive_code = code
328
- @last_interactive_output = @executor.last_output
329
- @last_interactive_result = exec_result ? exec_result.inspect : nil
330
- @last_interactive_executed = true
331
- end
332
-
333
- if @executor.last_cancelled?
334
- @history << { role: :user, content: "User declined to execute the code." }
335
- else
336
- output_parts = []
337
-
338
- # Capture printed output (puts, print, etc.)
339
- if @executor.last_output && !@executor.last_output.strip.empty?
340
- output_parts << "Output:\n#{@executor.last_output.strip}"
341
- end
342
-
343
- # Capture return value
344
- if exec_result
345
- output_parts << "Return value: #{exec_result.inspect}"
346
- end
347
-
348
- unless output_parts.empty?
349
- result_str = output_parts.join("\n\n")
350
- result_str = result_str[0..1000] + '...' if result_str.length > 1000
351
- @history << { role: :user, content: "Code was executed. #{result_str}" }
352
- end
353
- 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
354
338
  end
355
339
 
356
340
  # Update with the AI response, tokens, and any execution results
357
341
  log_interactive_turn
342
+
343
+ warn_if_history_large
358
344
  end
359
345
 
360
346
  $stdout = @interactive_old_stdout
@@ -374,6 +360,73 @@ module ConsoleAgent
374
360
  $stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
375
361
  end
376
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
+
377
430
  def provider
378
431
  @provider ||= Providers.build
379
432
  end
@@ -383,7 +436,32 @@ module ConsoleAgent
383
436
  end
384
437
 
385
438
  def context
386
- @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
387
465
  end
388
466
 
389
467
  def init_system_prompt(existing_guide)
@@ -805,6 +883,64 @@ module ConsoleAgent
805
883
  $stdout.puts "\e[2m[session totals — in: #{@total_input_tokens} | out: #{@total_output_tokens} | total: #{@total_input_tokens + @total_output_tokens}]\e[0m"
806
884
  end
807
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
+
808
944
  def display_exit_info
809
945
  display_session_summary
810
946
  if @interactive_session_id
@@ -339,8 +339,12 @@ module ConsoleAgent
339
339
  # Make result available as step1, step2, etc. for subsequent steps
340
340
  @executor.binding_context.local_variable_set(:"step#{i + 1}", exec_result)
341
341
  output = @executor.last_output
342
+ error = @executor.last_error
342
343
 
343
344
  step_report = "Step #{i + 1} (#{step['description']}):\n"
345
+ if error
346
+ step_report += "ERROR: #{error}\n"
347
+ end
344
348
  if output && !output.strip.empty?
345
349
  step_report += "Output: #{output.strip}\n"
346
350
  end
@@ -1,3 +1,3 @@
1
1
  module ConsoleAgent
2
- VERSION = '0.6.0'.freeze
2
+ VERSION = '0.7.0'.freeze
3
3
  end
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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cortfr