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 +4 -4
- data/README.md +46 -197
- data/lib/console_agent/console_methods.rb +13 -8
- data/lib/console_agent/executor.rb +6 -3
- data/lib/console_agent/repl.rb +206 -70
- data/lib/console_agent/tools/registry.rb +4 -0
- data/lib/console_agent/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 662ece5e5732e350b22871c8029421edf06ed4d9f98183f5cee7e95a3c1099f6
|
|
4
|
+
data.tar.gz: 483642598de39d23ace26f5d90863a8712b8799357c7809b2ba770f2bdc2522a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
```bash
|
|
44
|
+
bundle install
|
|
45
|
+
rails generate console_agent:install
|
|
112
46
|
```
|
|
113
47
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
### Application Guide
|
|
48
|
+
Set your API key in the generated initializer or via env var (`ANTHROPIC_API_KEY`):
|
|
117
49
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
57
|
+
## Commands
|
|
137
58
|
|
|
138
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/console_agent/repl.rb
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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:
|
|
37
|
+
conversation: conversation,
|
|
42
38
|
mode: 'one_shot',
|
|
43
|
-
code_executed:
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
@
|
|
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
|