console_agent 0.3.0 → 0.5.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 +16 -1
- data/lib/console_agent/console_methods.rb +10 -3
- data/lib/console_agent/repl.rb +113 -17
- data/lib/console_agent/tools/registry.rb +1 -1
- 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: 35e4cf8ecceb5241ce1b18f2cac6d8a235b3185f7839b2143eca6233390375d5
|
|
4
|
+
data.tar.gz: 7ebfc01003d9995ce65645a1f15955747cd59197c891e294d2152f3101fd583c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1bfdf0d1b5a6278101d7cda640ac710a1aef589e5a1adc4194876404d897a53bcfa27cd423029abc76a117eb6ab2ba13a14e605c4447c1452ab78d0f7b98b960
|
|
7
|
+
data.tar.gz: 7589590b8dcbfd4268070ce59da381741ca1643152b2689829d741a6cc325e7d9130576629a3155ba0267fb0ea86623a584f9a6e6c2956143e043da43e551501
|
data/README.md
CHANGED
|
@@ -117,7 +117,7 @@ Next time, it already knows — no re-reading files, fewer tokens.
|
|
|
117
117
|
```
|
|
118
118
|
irb> ai!
|
|
119
119
|
ConsoleAgent interactive mode. Type 'exit' to leave.
|
|
120
|
-
Auto-execute: OFF (Shift-Tab or /auto to toggle)
|
|
120
|
+
Auto-execute: OFF (Shift-Tab or /auto to toggle) | > code to run directly | /usage | /name <label>
|
|
121
121
|
|
|
122
122
|
ai> show me all tables
|
|
123
123
|
...
|
|
@@ -132,6 +132,21 @@ ai> exit
|
|
|
132
132
|
|
|
133
133
|
Toggle `/auto` to skip confirmation prompts. `/debug` shows raw API traffic. `/usage` shows token stats.
|
|
134
134
|
|
|
135
|
+
### Direct Code Execution
|
|
136
|
+
|
|
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...
|
|
144
|
+
|
|
145
|
+
You have **8 users** in your database, as confirmed by the `User.count` you just ran.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Useful for quick checks, setting up variables, or giving the AI concrete data to work with.
|
|
149
|
+
|
|
135
150
|
### Sessions
|
|
136
151
|
|
|
137
152
|
Sessions are saved automatically when session logging is enabled. You can name, list, and resume them.
|
|
@@ -84,7 +84,7 @@ module ConsoleAgent
|
|
|
84
84
|
nil
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
def ai_resume(identifier)
|
|
87
|
+
def ai_resume(identifier = nil)
|
|
88
88
|
__ensure_console_agent_user
|
|
89
89
|
|
|
90
90
|
require 'console_agent/context_builder'
|
|
@@ -93,9 +93,16 @@ module ConsoleAgent
|
|
|
93
93
|
require 'console_agent/repl'
|
|
94
94
|
require 'console_agent/session_logger'
|
|
95
95
|
|
|
96
|
-
session =
|
|
96
|
+
session = if identifier
|
|
97
|
+
__find_session(identifier)
|
|
98
|
+
else
|
|
99
|
+
session_class = Object.const_get('ConsoleAgent::Session')
|
|
100
|
+
session_class.where(mode: 'interactive', user_name: ConsoleAgent.current_user).recent.first
|
|
101
|
+
end
|
|
102
|
+
|
|
97
103
|
unless session
|
|
98
|
-
|
|
104
|
+
msg = identifier ? "Session not found: #{identifier}" : "No interactive sessions found."
|
|
105
|
+
$stderr.puts "\e[31m#{msg}\e[0m"
|
|
99
106
|
return nil
|
|
100
107
|
end
|
|
101
108
|
|
data/lib/console_agent/repl.rb
CHANGED
|
@@ -106,17 +106,17 @@ module ConsoleAgent
|
|
|
106
106
|
@total_input_tokens = session.input_tokens || 0
|
|
107
107
|
@total_output_tokens = session.output_tokens || 0
|
|
108
108
|
|
|
109
|
-
#
|
|
109
|
+
# Seed the capture buffer with previous output so it's preserved on save
|
|
110
|
+
@interactive_console_capture.write(session.console_output.to_s)
|
|
111
|
+
|
|
112
|
+
# Replay to the user via the real stdout (bypass TeeIO to avoid double-capture)
|
|
110
113
|
if session.console_output && !session.console_output.strip.empty?
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
@interactive_old_stdout.puts "\e[2m--- Replaying previous session output ---\e[0m"
|
|
115
|
+
@interactive_old_stdout.puts session.console_output
|
|
116
|
+
@interactive_old_stdout.puts "\e[2m--- End of previous output ---\e[0m"
|
|
117
|
+
@interactive_old_stdout.puts
|
|
115
118
|
end
|
|
116
119
|
|
|
117
|
-
# Copy replayed output into the capture buffer so it's preserved on save
|
|
118
|
-
@interactive_console_capture.write(session.console_output.to_s)
|
|
119
|
-
|
|
120
120
|
interactive_loop
|
|
121
121
|
end
|
|
122
122
|
|
|
@@ -144,8 +144,9 @@ module ConsoleAgent
|
|
|
144
144
|
def interactive_loop
|
|
145
145
|
auto = ConsoleAgent.configuration.auto_execute
|
|
146
146
|
name_display = @interactive_session_name ? " (#{@interactive_session_name})" : ""
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
# Write banner to real stdout (bypass TeeIO) so it doesn't accumulate on resume
|
|
148
|
+
@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"
|
|
149
150
|
|
|
150
151
|
# Bind Shift-Tab to insert /auto command and submit
|
|
151
152
|
if Readline.respond_to?(:parse_and_bind)
|
|
@@ -153,7 +154,7 @@ module ConsoleAgent
|
|
|
153
154
|
end
|
|
154
155
|
|
|
155
156
|
loop do
|
|
156
|
-
input = Readline.readline("\e[
|
|
157
|
+
input = Readline.readline("\001\e[33m\002ai> \001\e[0m\002", false)
|
|
157
158
|
break if input.nil? # Ctrl-D
|
|
158
159
|
|
|
159
160
|
input = input.strip
|
|
@@ -163,7 +164,7 @@ module ConsoleAgent
|
|
|
163
164
|
if input == '/auto'
|
|
164
165
|
ConsoleAgent.configuration.auto_execute = !ConsoleAgent.configuration.auto_execute
|
|
165
166
|
mode = ConsoleAgent.configuration.auto_execute ? 'ON' : 'OFF'
|
|
166
|
-
|
|
167
|
+
@interactive_old_stdout.puts "\e[36m Auto-execute: #{mode}\e[0m"
|
|
167
168
|
next
|
|
168
169
|
end
|
|
169
170
|
|
|
@@ -175,7 +176,7 @@ module ConsoleAgent
|
|
|
175
176
|
if input == '/debug'
|
|
176
177
|
ConsoleAgent.configuration.debug = !ConsoleAgent.configuration.debug
|
|
177
178
|
mode = ConsoleAgent.configuration.debug ? 'ON' : 'OFF'
|
|
178
|
-
|
|
179
|
+
@interactive_old_stdout.puts "\e[36m Debug: #{mode}\e[0m"
|
|
179
180
|
next
|
|
180
181
|
end
|
|
181
182
|
|
|
@@ -183,9 +184,9 @@ module ConsoleAgent
|
|
|
183
184
|
name = input.sub('/name', '').strip
|
|
184
185
|
if name.empty?
|
|
185
186
|
if @interactive_session_name
|
|
186
|
-
|
|
187
|
+
@interactive_old_stdout.puts "\e[36m Session name: #{@interactive_session_name}\e[0m"
|
|
187
188
|
else
|
|
188
|
-
|
|
189
|
+
@interactive_old_stdout.puts "\e[33m Usage: /name <label> (e.g. /name salesforce_user_123)\e[0m"
|
|
189
190
|
end
|
|
190
191
|
else
|
|
191
192
|
@interactive_session_name = name
|
|
@@ -193,11 +194,40 @@ module ConsoleAgent
|
|
|
193
194
|
require 'console_agent/session_logger'
|
|
194
195
|
SessionLogger.update(@interactive_session_id, name: name)
|
|
195
196
|
end
|
|
196
|
-
|
|
197
|
+
@interactive_old_stdout.puts "\e[36m Session named: #{name}\e[0m"
|
|
197
198
|
end
|
|
198
199
|
next
|
|
199
200
|
end
|
|
200
201
|
|
|
202
|
+
# Direct code execution with ">" prefix — skip LLM entirely
|
|
203
|
+
if input.start_with?('>') && !input.start_with?('>=')
|
|
204
|
+
raw_code = input.sub(/\A>\s?/, '')
|
|
205
|
+
Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
|
|
206
|
+
@interactive_console_capture.write("ai> #{input}\n")
|
|
207
|
+
|
|
208
|
+
exec_result = @executor.execute(raw_code)
|
|
209
|
+
|
|
210
|
+
output_parts = []
|
|
211
|
+
output_parts << "Output:\n#{@executor.last_output.strip}" if @executor.last_output && !@executor.last_output.strip.empty?
|
|
212
|
+
output_parts << "Return value: #{exec_result.inspect}" if exec_result
|
|
213
|
+
|
|
214
|
+
result_str = output_parts.join("\n\n")
|
|
215
|
+
result_str = result_str[0..1000] + '...' if result_str.length > 1000
|
|
216
|
+
|
|
217
|
+
context_msg = "User directly executed code: `#{raw_code}`"
|
|
218
|
+
context_msg += "\n#{result_str}" unless output_parts.empty?
|
|
219
|
+
@history << { role: :user, content: context_msg }
|
|
220
|
+
|
|
221
|
+
@interactive_query ||= input
|
|
222
|
+
@last_interactive_code = raw_code
|
|
223
|
+
@last_interactive_output = @executor.last_output
|
|
224
|
+
@last_interactive_result = exec_result ? exec_result.inspect : nil
|
|
225
|
+
@last_interactive_executed = true
|
|
226
|
+
|
|
227
|
+
log_interactive_turn
|
|
228
|
+
next
|
|
229
|
+
end
|
|
230
|
+
|
|
201
231
|
# Add to Readline history (avoid consecutive duplicates)
|
|
202
232
|
Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
|
|
203
233
|
|
|
@@ -328,7 +358,20 @@ module ConsoleAgent
|
|
|
328
358
|
$stdout.puts "\e[2m Thinking...\e[0m"
|
|
329
359
|
end
|
|
330
360
|
|
|
331
|
-
|
|
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
|
|
374
|
+
end
|
|
332
375
|
total_input += result.input_tokens || 0
|
|
333
376
|
total_output += result.output_tokens || 0
|
|
334
377
|
|
|
@@ -390,6 +433,59 @@ module ConsoleAgent
|
|
|
390
433
|
[final_result, new_messages]
|
|
391
434
|
end
|
|
392
435
|
|
|
436
|
+
# Monitors stdin for Escape (or Ctrl+C, since raw mode disables signals)
|
|
437
|
+
# and raises Interrupt in the main thread when detected.
|
|
438
|
+
def with_escape_monitoring
|
|
439
|
+
require 'io/console'
|
|
440
|
+
return yield unless $stdin.respond_to?(:raw)
|
|
441
|
+
|
|
442
|
+
monitor = Thread.new do
|
|
443
|
+
Thread.current.report_on_exception = false
|
|
444
|
+
$stdin.raw do |io|
|
|
445
|
+
loop do
|
|
446
|
+
break if Thread.current[:stop]
|
|
447
|
+
ready = IO.select([io], nil, nil, 0.2)
|
|
448
|
+
next unless ready
|
|
449
|
+
|
|
450
|
+
char = io.read_nonblock(1) rescue nil
|
|
451
|
+
next unless char
|
|
452
|
+
|
|
453
|
+
if char == "\x03" # Ctrl+C (raw mode eats the signal)
|
|
454
|
+
Thread.main.raise(Interrupt)
|
|
455
|
+
break
|
|
456
|
+
elsif char == "\e"
|
|
457
|
+
# Distinguish standalone Escape from escape sequences (arrow keys, etc.)
|
|
458
|
+
seq = IO.select([io], nil, nil, 0.05)
|
|
459
|
+
if seq
|
|
460
|
+
io.read_nonblock(10) rescue nil # consume the sequence
|
|
461
|
+
else
|
|
462
|
+
Thread.main.raise(Interrupt)
|
|
463
|
+
break
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
rescue IOError, Errno::EIO, Errno::ENODEV, Errno::ENOTTY
|
|
469
|
+
# stdin is not a TTY (e.g. in tests or piped input) — silently skip
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
begin
|
|
473
|
+
yield
|
|
474
|
+
ensure
|
|
475
|
+
monitor[:stop] = true
|
|
476
|
+
monitor.join(1) rescue nil
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
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
|
|
487
|
+
end
|
|
488
|
+
|
|
393
489
|
def format_tool_args(name, args)
|
|
394
490
|
return '' if args.nil? || args.empty?
|
|
395
491
|
|
|
@@ -285,7 +285,7 @@ module ConsoleAgent
|
|
|
285
285
|
when 'a', 'auto'
|
|
286
286
|
skip_confirmations = true
|
|
287
287
|
when 'y', 'yes'
|
|
288
|
-
|
|
288
|
+
skip_confirmations = true if steps.length == 1
|
|
289
289
|
else
|
|
290
290
|
$stdout.puts "\e[33m Plan declined.\e[0m"
|
|
291
291
|
feedback = ask_feedback("What would you like changed?")
|