console_agent 0.2.0 → 0.4.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 +117 -231
- data/app/controllers/console_agent/application_controller.rb +21 -0
- data/app/controllers/console_agent/sessions_controller.rb +16 -0
- data/app/helpers/console_agent/sessions_helper.rb +42 -0
- data/app/models/console_agent/session.rb +23 -0
- data/app/views/console_agent/sessions/index.html.erb +57 -0
- data/app/views/console_agent/sessions/show.html.erb +56 -0
- data/app/views/layouts/console_agent/application.html.erb +83 -0
- data/config/routes.rb +4 -0
- data/lib/console_agent/configuration.rb +8 -4
- data/lib/console_agent/console_methods.rb +132 -5
- data/lib/console_agent/context_builder.rb +12 -132
- data/lib/console_agent/engine.rb +5 -0
- data/lib/console_agent/executor.rb +19 -1
- data/lib/console_agent/repl.rb +368 -36
- data/lib/console_agent/session_logger.rb +79 -0
- data/lib/console_agent/tools/registry.rb +156 -2
- data/lib/console_agent/version.rb +1 -1
- data/lib/console_agent.rb +125 -3
- data/lib/generators/console_agent/templates/initializer.rb +14 -6
- metadata +11 -1
data/lib/console_agent/repl.rb
CHANGED
|
@@ -15,17 +15,44 @@ module ConsoleAgent
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def one_shot(query)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
19
|
+
console_capture = StringIO.new
|
|
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?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@_last_log_attrs = {
|
|
40
|
+
query: query,
|
|
41
|
+
conversation: [{ role: :user, content: query }, { role: :assistant, content: result.text }],
|
|
42
|
+
mode: 'one_shot',
|
|
43
|
+
code_executed: has_code ? code : nil,
|
|
44
|
+
code_output: executed ? @executor.last_output : nil,
|
|
45
|
+
code_result: executed && exec_result ? exec_result.inspect : nil,
|
|
46
|
+
executed: executed,
|
|
47
|
+
start_time: start_time
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
exec_result
|
|
28
51
|
end
|
|
52
|
+
|
|
53
|
+
log_session(@_last_log_attrs.merge(console_output: console_capture.string))
|
|
54
|
+
|
|
55
|
+
exec_result
|
|
29
56
|
rescue Providers::ProviderError => e
|
|
30
57
|
$stderr.puts "\e[31mConsoleAgent Error: #{e.message}\e[0m"
|
|
31
58
|
nil
|
|
@@ -35,10 +62,25 @@ module ConsoleAgent
|
|
|
35
62
|
end
|
|
36
63
|
|
|
37
64
|
def explain(query)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
65
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
66
|
+
console_capture = StringIO.new
|
|
67
|
+
with_console_capture(console_capture) do
|
|
68
|
+
result, _ = send_query(query)
|
|
69
|
+
track_usage(result)
|
|
70
|
+
@executor.display_response(result.text)
|
|
71
|
+
display_usage(result)
|
|
72
|
+
|
|
73
|
+
@_last_log_attrs = {
|
|
74
|
+
query: query,
|
|
75
|
+
conversation: [{ role: :user, content: query }, { role: :assistant, content: result.text }],
|
|
76
|
+
mode: 'explain',
|
|
77
|
+
executed: false,
|
|
78
|
+
start_time: start_time
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
log_session(@_last_log_attrs.merge(console_output: console_capture.string))
|
|
83
|
+
|
|
42
84
|
nil
|
|
43
85
|
rescue Providers::ProviderError => e
|
|
44
86
|
$stderr.puts "\e[31mConsoleAgent Error: #{e.message}\e[0m"
|
|
@@ -49,29 +91,132 @@ module ConsoleAgent
|
|
|
49
91
|
end
|
|
50
92
|
|
|
51
93
|
def interactive
|
|
52
|
-
|
|
94
|
+
init_interactive_state
|
|
95
|
+
interactive_loop
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def resume(session)
|
|
99
|
+
init_interactive_state
|
|
100
|
+
|
|
101
|
+
# Restore state from the previous session
|
|
102
|
+
@history = JSON.parse(session.conversation, symbolize_names: true)
|
|
103
|
+
@interactive_session_id = session.id
|
|
104
|
+
@interactive_query = session.query
|
|
105
|
+
@interactive_session_name = session.name
|
|
106
|
+
@total_input_tokens = session.input_tokens || 0
|
|
107
|
+
@total_output_tokens = session.output_tokens || 0
|
|
108
|
+
|
|
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)
|
|
113
|
+
if session.console_output && !session.console_output.strip.empty?
|
|
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
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
interactive_loop
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def init_interactive_state
|
|
126
|
+
@interactive_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
127
|
+
@interactive_console_capture = StringIO.new
|
|
128
|
+
@interactive_old_stdout = $stdout
|
|
129
|
+
$stdout = TeeIO.new(@interactive_old_stdout, @interactive_console_capture)
|
|
130
|
+
@executor.on_prompt = -> { log_interactive_turn }
|
|
131
|
+
|
|
53
132
|
@history = []
|
|
54
133
|
@total_input_tokens = 0
|
|
55
134
|
@total_output_tokens = 0
|
|
135
|
+
@interactive_query = nil
|
|
136
|
+
@interactive_session_id = nil
|
|
137
|
+
@interactive_session_name = nil
|
|
138
|
+
@last_interactive_code = nil
|
|
139
|
+
@last_interactive_output = nil
|
|
140
|
+
@last_interactive_result = nil
|
|
141
|
+
@last_interactive_executed = false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def interactive_loop
|
|
145
|
+
auto = ConsoleAgent.configuration.auto_execute
|
|
146
|
+
name_display = @interactive_session_name ? " (#{@interactive_session_name})" : ""
|
|
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) | /usage | /name <label>\e[0m"
|
|
150
|
+
|
|
151
|
+
# Bind Shift-Tab to insert /auto command and submit
|
|
152
|
+
if Readline.respond_to?(:parse_and_bind)
|
|
153
|
+
Readline.parse_and_bind('"\e[Z": "\C-a\C-k/auto\C-m"')
|
|
154
|
+
end
|
|
56
155
|
|
|
57
156
|
loop do
|
|
58
|
-
input = Readline.readline("\e[
|
|
157
|
+
input = Readline.readline("\001\e[33m\002ai> \001\e[0m\002", false)
|
|
59
158
|
break if input.nil? # Ctrl-D
|
|
60
159
|
|
|
61
160
|
input = input.strip
|
|
62
161
|
break if input.downcase == 'exit' || input.downcase == 'quit'
|
|
63
162
|
next if input.empty?
|
|
64
163
|
|
|
164
|
+
if input == '/auto'
|
|
165
|
+
ConsoleAgent.configuration.auto_execute = !ConsoleAgent.configuration.auto_execute
|
|
166
|
+
mode = ConsoleAgent.configuration.auto_execute ? 'ON' : 'OFF'
|
|
167
|
+
@interactive_old_stdout.puts "\e[36m Auto-execute: #{mode}\e[0m"
|
|
168
|
+
next
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
if input == '/usage'
|
|
172
|
+
display_session_summary
|
|
173
|
+
next
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if input == '/debug'
|
|
177
|
+
ConsoleAgent.configuration.debug = !ConsoleAgent.configuration.debug
|
|
178
|
+
mode = ConsoleAgent.configuration.debug ? 'ON' : 'OFF'
|
|
179
|
+
@interactive_old_stdout.puts "\e[36m Debug: #{mode}\e[0m"
|
|
180
|
+
next
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
if input.start_with?('/name')
|
|
184
|
+
name = input.sub('/name', '').strip
|
|
185
|
+
if name.empty?
|
|
186
|
+
if @interactive_session_name
|
|
187
|
+
@interactive_old_stdout.puts "\e[36m Session name: #{@interactive_session_name}\e[0m"
|
|
188
|
+
else
|
|
189
|
+
@interactive_old_stdout.puts "\e[33m Usage: /name <label> (e.g. /name salesforce_user_123)\e[0m"
|
|
190
|
+
end
|
|
191
|
+
else
|
|
192
|
+
@interactive_session_name = name
|
|
193
|
+
if @interactive_session_id
|
|
194
|
+
require 'console_agent/session_logger'
|
|
195
|
+
SessionLogger.update(@interactive_session_id, name: name)
|
|
196
|
+
end
|
|
197
|
+
@interactive_old_stdout.puts "\e[36m Session named: #{name}\e[0m"
|
|
198
|
+
end
|
|
199
|
+
next
|
|
200
|
+
end
|
|
201
|
+
|
|
65
202
|
# Add to Readline history (avoid consecutive duplicates)
|
|
66
203
|
Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
|
|
67
204
|
|
|
205
|
+
@interactive_query ||= input
|
|
68
206
|
@history << { role: :user, content: input }
|
|
69
207
|
|
|
208
|
+
# Log the user's prompt line to the console capture (Readline doesn't go through $stdout)
|
|
209
|
+
@interactive_console_capture.write("ai> #{input}\n")
|
|
210
|
+
|
|
211
|
+
# Save immediately so the session is visible in the admin UI while the AI thinks
|
|
212
|
+
log_interactive_turn
|
|
213
|
+
|
|
70
214
|
begin
|
|
71
|
-
result = send_query(input, conversation: @history)
|
|
215
|
+
result, tool_messages = send_query(input, conversation: @history)
|
|
72
216
|
rescue Interrupt
|
|
73
217
|
$stdout.puts "\n\e[33m Aborted.\e[0m"
|
|
74
218
|
@history.pop # Remove the user message that never got a response
|
|
219
|
+
log_interactive_turn
|
|
75
220
|
next
|
|
76
221
|
end
|
|
77
222
|
|
|
@@ -79,6 +224,11 @@ module ConsoleAgent
|
|
|
79
224
|
code = @executor.display_response(result.text)
|
|
80
225
|
display_usage(result, show_session: true)
|
|
81
226
|
|
|
227
|
+
# Save after response is displayed so viewer shows progress before Execute prompt
|
|
228
|
+
log_interactive_turn
|
|
229
|
+
|
|
230
|
+
# Add tool call/result messages so the LLM remembers what it learned
|
|
231
|
+
@history.concat(tool_messages) if tool_messages && !tool_messages.empty?
|
|
82
232
|
@history << { role: :assistant, content: result.text }
|
|
83
233
|
|
|
84
234
|
if code && !code.strip.empty?
|
|
@@ -88,6 +238,13 @@ module ConsoleAgent
|
|
|
88
238
|
exec_result = @executor.confirm_and_execute(code)
|
|
89
239
|
end
|
|
90
240
|
|
|
241
|
+
unless @executor.last_cancelled?
|
|
242
|
+
@last_interactive_code = code
|
|
243
|
+
@last_interactive_output = @executor.last_output
|
|
244
|
+
@last_interactive_result = exec_result ? exec_result.inspect : nil
|
|
245
|
+
@last_interactive_executed = true
|
|
246
|
+
end
|
|
247
|
+
|
|
91
248
|
if @executor.last_cancelled?
|
|
92
249
|
@history << { role: :user, content: "User declined to execute the code." }
|
|
93
250
|
else
|
|
@@ -110,21 +267,28 @@ module ConsoleAgent
|
|
|
110
267
|
end
|
|
111
268
|
end
|
|
112
269
|
end
|
|
270
|
+
|
|
271
|
+
# Update with the AI response, tokens, and any execution results
|
|
272
|
+
log_interactive_turn
|
|
113
273
|
end
|
|
114
274
|
|
|
115
|
-
|
|
116
|
-
|
|
275
|
+
$stdout = @interactive_old_stdout
|
|
276
|
+
@executor.on_prompt = nil
|
|
277
|
+
finish_interactive_session
|
|
278
|
+
display_exit_info
|
|
117
279
|
rescue Interrupt
|
|
118
280
|
# Ctrl-C during Readline input — exit cleanly
|
|
281
|
+
$stdout = @interactive_old_stdout if @interactive_old_stdout
|
|
282
|
+
@executor.on_prompt = nil
|
|
119
283
|
$stdout.puts
|
|
120
|
-
|
|
121
|
-
|
|
284
|
+
finish_interactive_session
|
|
285
|
+
display_exit_info
|
|
122
286
|
rescue => e
|
|
287
|
+
$stdout = @interactive_old_stdout if @interactive_old_stdout
|
|
288
|
+
@executor.on_prompt = nil
|
|
123
289
|
$stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
|
|
124
290
|
end
|
|
125
291
|
|
|
126
|
-
private
|
|
127
|
-
|
|
128
292
|
def provider
|
|
129
293
|
@provider ||= Providers.build
|
|
130
294
|
end
|
|
@@ -141,25 +305,22 @@ module ConsoleAgent
|
|
|
141
305
|
ConsoleAgent.configuration.validate!
|
|
142
306
|
|
|
143
307
|
messages = if conversation
|
|
144
|
-
conversation.
|
|
308
|
+
conversation.dup
|
|
145
309
|
else
|
|
146
310
|
[{ role: :user, content: query }]
|
|
147
311
|
end
|
|
148
312
|
|
|
149
|
-
|
|
150
|
-
send_query_with_tools(messages)
|
|
151
|
-
else
|
|
152
|
-
provider.chat(messages, system_prompt: context)
|
|
153
|
-
end
|
|
313
|
+
send_query_with_tools(messages)
|
|
154
314
|
end
|
|
155
315
|
|
|
156
316
|
def send_query_with_tools(messages)
|
|
157
317
|
require 'console_agent/tools/registry'
|
|
158
|
-
tools = Tools::Registry.new
|
|
318
|
+
tools = Tools::Registry.new(executor: @executor)
|
|
159
319
|
max_rounds = ConsoleAgent.configuration.max_tool_rounds
|
|
160
320
|
total_input = 0
|
|
161
321
|
total_output = 0
|
|
162
322
|
result = nil
|
|
323
|
+
new_messages = [] # Track messages added during tool use
|
|
163
324
|
|
|
164
325
|
exhausted = false
|
|
165
326
|
|
|
@@ -168,7 +329,20 @@ module ConsoleAgent
|
|
|
168
329
|
$stdout.puts "\e[2m Thinking...\e[0m"
|
|
169
330
|
end
|
|
170
331
|
|
|
171
|
-
|
|
332
|
+
begin
|
|
333
|
+
result = with_escape_monitoring do
|
|
334
|
+
provider.chat_with_tools(messages, tools: tools, system_prompt: context)
|
|
335
|
+
end
|
|
336
|
+
rescue Interrupt
|
|
337
|
+
redirect = prompt_for_redirect
|
|
338
|
+
if redirect
|
|
339
|
+
messages << { role: :user, content: redirect }
|
|
340
|
+
new_messages << messages.last
|
|
341
|
+
next
|
|
342
|
+
else
|
|
343
|
+
raise
|
|
344
|
+
end
|
|
345
|
+
end
|
|
172
346
|
total_input += result.input_tokens || 0
|
|
173
347
|
total_output += result.output_tokens || 0
|
|
174
348
|
|
|
@@ -180,12 +354,14 @@ module ConsoleAgent
|
|
|
180
354
|
end
|
|
181
355
|
|
|
182
356
|
# Add assistant message with tool calls to conversation
|
|
183
|
-
|
|
357
|
+
assistant_msg = provider.format_assistant_message(result)
|
|
358
|
+
messages << assistant_msg
|
|
359
|
+
new_messages << assistant_msg
|
|
184
360
|
|
|
185
361
|
# Execute each tool and show progress
|
|
186
362
|
result.tool_calls.each do |tc|
|
|
187
|
-
# ask_user
|
|
188
|
-
if tc[:name] == 'ask_user'
|
|
363
|
+
# ask_user and execute_plan handle their own display
|
|
364
|
+
if tc[:name] == 'ask_user' || tc[:name] == 'execute_plan'
|
|
189
365
|
tool_result = tools.execute(tc[:name], tc[:arguments])
|
|
190
366
|
else
|
|
191
367
|
args_display = format_tool_args(tc[:name], tc[:arguments])
|
|
@@ -202,7 +378,9 @@ module ConsoleAgent
|
|
|
202
378
|
$stderr.puts "\e[35m[debug tool result] #{tool_result}\e[0m"
|
|
203
379
|
end
|
|
204
380
|
|
|
205
|
-
|
|
381
|
+
tool_msg = provider.format_tool_result(tc[:id], tool_result)
|
|
382
|
+
messages << tool_msg
|
|
383
|
+
new_messages << tool_msg
|
|
206
384
|
end
|
|
207
385
|
|
|
208
386
|
exhausted = true if round == max_rounds - 1
|
|
@@ -217,12 +395,66 @@ module ConsoleAgent
|
|
|
217
395
|
total_output += result.output_tokens || 0
|
|
218
396
|
end
|
|
219
397
|
|
|
220
|
-
Providers::ChatResult.new(
|
|
398
|
+
final_result = Providers::ChatResult.new(
|
|
221
399
|
text: result ? result.text : '',
|
|
222
400
|
input_tokens: total_input,
|
|
223
401
|
output_tokens: total_output,
|
|
224
402
|
stop_reason: result ? result.stop_reason : :end_turn
|
|
225
403
|
)
|
|
404
|
+
[final_result, new_messages]
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Monitors stdin for Escape (or Ctrl+C, since raw mode disables signals)
|
|
408
|
+
# and raises Interrupt in the main thread when detected.
|
|
409
|
+
def with_escape_monitoring
|
|
410
|
+
require 'io/console'
|
|
411
|
+
return yield unless $stdin.respond_to?(:raw)
|
|
412
|
+
|
|
413
|
+
monitor = Thread.new do
|
|
414
|
+
Thread.current.report_on_exception = false
|
|
415
|
+
$stdin.raw do |io|
|
|
416
|
+
loop do
|
|
417
|
+
break if Thread.current[:stop]
|
|
418
|
+
ready = IO.select([io], nil, nil, 0.2)
|
|
419
|
+
next unless ready
|
|
420
|
+
|
|
421
|
+
char = io.read_nonblock(1) rescue nil
|
|
422
|
+
next unless char
|
|
423
|
+
|
|
424
|
+
if char == "\x03" # Ctrl+C (raw mode eats the signal)
|
|
425
|
+
Thread.main.raise(Interrupt)
|
|
426
|
+
break
|
|
427
|
+
elsif char == "\e"
|
|
428
|
+
# Distinguish standalone Escape from escape sequences (arrow keys, etc.)
|
|
429
|
+
seq = IO.select([io], nil, nil, 0.05)
|
|
430
|
+
if seq
|
|
431
|
+
io.read_nonblock(10) rescue nil # consume the sequence
|
|
432
|
+
else
|
|
433
|
+
Thread.main.raise(Interrupt)
|
|
434
|
+
break
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
rescue IOError, Errno::EIO, Errno::ENODEV, Errno::ENOTTY
|
|
440
|
+
# stdin is not a TTY (e.g. in tests or piped input) — silently skip
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
begin
|
|
444
|
+
yield
|
|
445
|
+
ensure
|
|
446
|
+
monitor[:stop] = true
|
|
447
|
+
monitor.join(1) rescue nil
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def prompt_for_redirect
|
|
452
|
+
$stdout.puts "\n\e[33m Interrupted. What should the AI do differently?\e[0m"
|
|
453
|
+
$stdout.puts "\e[2m (Press Enter with no input to abort entirely)\e[0m"
|
|
454
|
+
$stdout.print "\e[33m redirect> \e[0m"
|
|
455
|
+
input = $stdin.gets
|
|
456
|
+
return nil if input.nil? || input.strip.empty?
|
|
457
|
+
input.strip
|
|
226
458
|
end
|
|
227
459
|
|
|
228
460
|
def format_tool_args(name, args)
|
|
@@ -246,6 +478,9 @@ module ConsoleAgent
|
|
|
246
478
|
"(\"#{args['name']}\")"
|
|
247
479
|
when 'recall_memories'
|
|
248
480
|
args['query'] ? "(\"#{args['query']}\")" : ''
|
|
481
|
+
when 'execute_plan'
|
|
482
|
+
steps = args['steps']
|
|
483
|
+
steps ? "(#{steps.length} steps)" : ''
|
|
249
484
|
else
|
|
250
485
|
''
|
|
251
486
|
end
|
|
@@ -300,6 +535,9 @@ module ConsoleAgent
|
|
|
300
535
|
when 'recall_memories'
|
|
301
536
|
chunks = result.split("\n\n")
|
|
302
537
|
chunks.length > 1 ? "#{chunks.length} memories found" : truncate(result, 80)
|
|
538
|
+
when 'execute_plan'
|
|
539
|
+
steps_done = result.scan(/^Step \d+/).length
|
|
540
|
+
steps_done > 0 ? "#{steps_done} steps executed" : truncate(result, 80)
|
|
303
541
|
else
|
|
304
542
|
truncate(result, 80)
|
|
305
543
|
end
|
|
@@ -333,10 +571,104 @@ module ConsoleAgent
|
|
|
333
571
|
$stdout.puts line
|
|
334
572
|
end
|
|
335
573
|
|
|
574
|
+
def with_console_capture(capture_io)
|
|
575
|
+
old_stdout = $stdout
|
|
576
|
+
$stdout = TeeIO.new(old_stdout, capture_io)
|
|
577
|
+
yield
|
|
578
|
+
ensure
|
|
579
|
+
$stdout = old_stdout
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def log_interactive_turn
|
|
583
|
+
require 'console_agent/session_logger'
|
|
584
|
+
session_attrs = {
|
|
585
|
+
conversation: @history,
|
|
586
|
+
input_tokens: @total_input_tokens,
|
|
587
|
+
output_tokens: @total_output_tokens,
|
|
588
|
+
code_executed: @last_interactive_code,
|
|
589
|
+
code_output: @last_interactive_output,
|
|
590
|
+
code_result: @last_interactive_result,
|
|
591
|
+
executed: @last_interactive_executed,
|
|
592
|
+
console_output: @interactive_console_capture&.string
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if @interactive_session_id
|
|
596
|
+
SessionLogger.update(@interactive_session_id, session_attrs)
|
|
597
|
+
else
|
|
598
|
+
@interactive_session_id = SessionLogger.log(
|
|
599
|
+
session_attrs.merge(
|
|
600
|
+
query: @interactive_query || '(interactive session)',
|
|
601
|
+
mode: 'interactive',
|
|
602
|
+
name: @interactive_session_name
|
|
603
|
+
)
|
|
604
|
+
)
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def finish_interactive_session
|
|
609
|
+
require 'console_agent/session_logger'
|
|
610
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @interactive_start) * 1000).round
|
|
611
|
+
if @interactive_session_id
|
|
612
|
+
SessionLogger.update(@interactive_session_id,
|
|
613
|
+
conversation: @history,
|
|
614
|
+
input_tokens: @total_input_tokens,
|
|
615
|
+
output_tokens: @total_output_tokens,
|
|
616
|
+
code_executed: @last_interactive_code,
|
|
617
|
+
code_output: @last_interactive_output,
|
|
618
|
+
code_result: @last_interactive_result,
|
|
619
|
+
executed: @last_interactive_executed,
|
|
620
|
+
console_output: @interactive_console_capture&.string,
|
|
621
|
+
duration_ms: duration_ms
|
|
622
|
+
)
|
|
623
|
+
elsif @interactive_query
|
|
624
|
+
# Session was never created (e.g., only one turn that failed to log)
|
|
625
|
+
log_session(
|
|
626
|
+
query: @interactive_query,
|
|
627
|
+
conversation: @history,
|
|
628
|
+
mode: 'interactive',
|
|
629
|
+
code_executed: @last_interactive_code,
|
|
630
|
+
code_output: @last_interactive_output,
|
|
631
|
+
code_result: @last_interactive_result,
|
|
632
|
+
executed: @last_interactive_executed,
|
|
633
|
+
console_output: @interactive_console_capture&.string,
|
|
634
|
+
start_time: @interactive_start
|
|
635
|
+
)
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def log_session(attrs)
|
|
640
|
+
require 'console_agent/session_logger'
|
|
641
|
+
start_time = attrs.delete(:start_time)
|
|
642
|
+
duration_ms = if start_time
|
|
643
|
+
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round
|
|
644
|
+
end
|
|
645
|
+
SessionLogger.log(
|
|
646
|
+
attrs.merge(
|
|
647
|
+
input_tokens: @total_input_tokens,
|
|
648
|
+
output_tokens: @total_output_tokens,
|
|
649
|
+
duration_ms: duration_ms
|
|
650
|
+
)
|
|
651
|
+
)
|
|
652
|
+
end
|
|
653
|
+
|
|
336
654
|
def display_session_summary
|
|
337
655
|
return if @total_input_tokens == 0 && @total_output_tokens == 0
|
|
338
656
|
|
|
339
657
|
$stdout.puts "\e[2m[session totals — in: #{@total_input_tokens} | out: #{@total_output_tokens} | total: #{@total_input_tokens + @total_output_tokens}]\e[0m"
|
|
340
658
|
end
|
|
659
|
+
|
|
660
|
+
def display_exit_info
|
|
661
|
+
display_session_summary
|
|
662
|
+
if @interactive_session_id
|
|
663
|
+
$stdout.puts "\e[36mSession ##{@interactive_session_id} saved.\e[0m"
|
|
664
|
+
if @interactive_session_name
|
|
665
|
+
$stdout.puts "\e[2m Resume with: ai_resume \"#{@interactive_session_name}\"\e[0m"
|
|
666
|
+
else
|
|
667
|
+
$stdout.puts "\e[2m Name it: ai_name #{@interactive_session_id}, \"descriptive_name\"\e[0m"
|
|
668
|
+
$stdout.puts "\e[2m Resume it: ai_resume #{@interactive_session_id}\e[0m"
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
$stdout.puts "\e[36mLeft ConsoleAgent interactive mode.\e[0m"
|
|
672
|
+
end
|
|
341
673
|
end
|
|
342
674
|
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module ConsoleAgent
|
|
2
|
+
module SessionLogger
|
|
3
|
+
class << self
|
|
4
|
+
def log(attrs)
|
|
5
|
+
return unless ConsoleAgent.configuration.session_logging
|
|
6
|
+
return unless table_exists?
|
|
7
|
+
|
|
8
|
+
record = session_class.create!(
|
|
9
|
+
query: attrs[:query],
|
|
10
|
+
conversation: Array(attrs[:conversation]).to_json,
|
|
11
|
+
input_tokens: attrs[:input_tokens] || 0,
|
|
12
|
+
output_tokens: attrs[:output_tokens] || 0,
|
|
13
|
+
user_name: current_user_name,
|
|
14
|
+
mode: attrs[:mode].to_s,
|
|
15
|
+
name: attrs[:name],
|
|
16
|
+
code_executed: attrs[:code_executed],
|
|
17
|
+
code_output: attrs[:code_output],
|
|
18
|
+
code_result: attrs[:code_result],
|
|
19
|
+
console_output: attrs[:console_output],
|
|
20
|
+
executed: attrs[:executed] || false,
|
|
21
|
+
provider: ConsoleAgent.configuration.provider.to_s,
|
|
22
|
+
model: ConsoleAgent.configuration.resolved_model,
|
|
23
|
+
duration_ms: attrs[:duration_ms],
|
|
24
|
+
created_at: Time.respond_to?(:current) ? Time.current : Time.now
|
|
25
|
+
)
|
|
26
|
+
record.id
|
|
27
|
+
rescue => e
|
|
28
|
+
msg = "ConsoleAgent: session logging failed: #{e.class}: #{e.message}"
|
|
29
|
+
$stderr.puts "\e[33m#{msg}\e[0m" if $stderr.respond_to?(:puts)
|
|
30
|
+
ConsoleAgent.logger.warn(msg)
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def update(id, attrs)
|
|
35
|
+
return unless id
|
|
36
|
+
return unless ConsoleAgent.configuration.session_logging
|
|
37
|
+
return unless table_exists?
|
|
38
|
+
|
|
39
|
+
updates = {}
|
|
40
|
+
updates[:conversation] = Array(attrs[:conversation]).to_json if attrs.key?(:conversation)
|
|
41
|
+
updates[:input_tokens] = attrs[:input_tokens] if attrs.key?(:input_tokens)
|
|
42
|
+
updates[:output_tokens] = attrs[:output_tokens] if attrs.key?(:output_tokens)
|
|
43
|
+
updates[:code_executed] = attrs[:code_executed] if attrs.key?(:code_executed)
|
|
44
|
+
updates[:code_output] = attrs[:code_output] if attrs.key?(:code_output)
|
|
45
|
+
updates[:code_result] = attrs[:code_result] if attrs.key?(:code_result)
|
|
46
|
+
updates[:console_output] = attrs[:console_output] if attrs.key?(:console_output)
|
|
47
|
+
updates[:executed] = attrs[:executed] if attrs.key?(:executed)
|
|
48
|
+
updates[:duration_ms] = attrs[:duration_ms] if attrs.key?(:duration_ms)
|
|
49
|
+
updates[:name] = attrs[:name] if attrs.key?(:name)
|
|
50
|
+
|
|
51
|
+
session_class.where(id: id).update_all(updates) unless updates.empty?
|
|
52
|
+
rescue => e
|
|
53
|
+
msg = "ConsoleAgent: session update failed: #{e.class}: #{e.message}"
|
|
54
|
+
$stderr.puts "\e[33m#{msg}\e[0m" if $stderr.respond_to?(:puts)
|
|
55
|
+
ConsoleAgent.logger.warn(msg)
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def table_exists?
|
|
62
|
+
# Only cache positive results — retry on failure so transient
|
|
63
|
+
# errors (boot timing, connection not ready) don't stick forever
|
|
64
|
+
return true if @table_exists
|
|
65
|
+
@table_exists = session_class.connection.table_exists?('console_agent_sessions')
|
|
66
|
+
rescue
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def session_class
|
|
71
|
+
Object.const_get('ConsoleAgent::Session')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def current_user_name
|
|
75
|
+
ConsoleAgent.current_user || ENV['USER']
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|