console_agent 0.2.0 → 0.3.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 +125 -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 +299 -34
- 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,10 +91,66 @@ 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
|
+
# Replay stored console output so the user sees previous context
|
|
110
|
+
if session.console_output && !session.console_output.strip.empty?
|
|
111
|
+
$stdout.puts "\e[2m--- Replaying previous session output ---\e[0m"
|
|
112
|
+
$stdout.puts session.console_output
|
|
113
|
+
$stdout.puts "\e[2m--- End of previous output ---\e[0m"
|
|
114
|
+
$stdout.puts
|
|
115
|
+
end
|
|
116
|
+
|
|
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
|
+
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
|
+
$stdout.puts "\e[36mConsoleAgent interactive mode#{name_display}. Type 'exit' or 'quit' to leave.\e[0m"
|
|
148
|
+
$stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle) | /usage | /name <label>\e[0m"
|
|
149
|
+
|
|
150
|
+
# Bind Shift-Tab to insert /auto command and submit
|
|
151
|
+
if Readline.respond_to?(:parse_and_bind)
|
|
152
|
+
Readline.parse_and_bind('"\e[Z": "\C-a\C-k/auto\C-m"')
|
|
153
|
+
end
|
|
56
154
|
|
|
57
155
|
loop do
|
|
58
156
|
input = Readline.readline("\e[33mai> \e[0m", false)
|
|
@@ -62,16 +160,62 @@ module ConsoleAgent
|
|
|
62
160
|
break if input.downcase == 'exit' || input.downcase == 'quit'
|
|
63
161
|
next if input.empty?
|
|
64
162
|
|
|
163
|
+
if input == '/auto'
|
|
164
|
+
ConsoleAgent.configuration.auto_execute = !ConsoleAgent.configuration.auto_execute
|
|
165
|
+
mode = ConsoleAgent.configuration.auto_execute ? 'ON' : 'OFF'
|
|
166
|
+
$stdout.puts "\e[36m Auto-execute: #{mode}\e[0m"
|
|
167
|
+
next
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if input == '/usage'
|
|
171
|
+
display_session_summary
|
|
172
|
+
next
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if input == '/debug'
|
|
176
|
+
ConsoleAgent.configuration.debug = !ConsoleAgent.configuration.debug
|
|
177
|
+
mode = ConsoleAgent.configuration.debug ? 'ON' : 'OFF'
|
|
178
|
+
$stdout.puts "\e[36m Debug: #{mode}\e[0m"
|
|
179
|
+
next
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if input.start_with?('/name')
|
|
183
|
+
name = input.sub('/name', '').strip
|
|
184
|
+
if name.empty?
|
|
185
|
+
if @interactive_session_name
|
|
186
|
+
$stdout.puts "\e[36m Session name: #{@interactive_session_name}\e[0m"
|
|
187
|
+
else
|
|
188
|
+
$stdout.puts "\e[33m Usage: /name <label> (e.g. /name salesforce_user_123)\e[0m"
|
|
189
|
+
end
|
|
190
|
+
else
|
|
191
|
+
@interactive_session_name = name
|
|
192
|
+
if @interactive_session_id
|
|
193
|
+
require 'console_agent/session_logger'
|
|
194
|
+
SessionLogger.update(@interactive_session_id, name: name)
|
|
195
|
+
end
|
|
196
|
+
$stdout.puts "\e[36m Session named: #{name}\e[0m"
|
|
197
|
+
end
|
|
198
|
+
next
|
|
199
|
+
end
|
|
200
|
+
|
|
65
201
|
# Add to Readline history (avoid consecutive duplicates)
|
|
66
202
|
Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
|
|
67
203
|
|
|
204
|
+
@interactive_query ||= input
|
|
68
205
|
@history << { role: :user, content: input }
|
|
69
206
|
|
|
207
|
+
# Log the user's prompt line to the console capture (Readline doesn't go through $stdout)
|
|
208
|
+
@interactive_console_capture.write("ai> #{input}\n")
|
|
209
|
+
|
|
210
|
+
# Save immediately so the session is visible in the admin UI while the AI thinks
|
|
211
|
+
log_interactive_turn
|
|
212
|
+
|
|
70
213
|
begin
|
|
71
|
-
result = send_query(input, conversation: @history)
|
|
214
|
+
result, tool_messages = send_query(input, conversation: @history)
|
|
72
215
|
rescue Interrupt
|
|
73
216
|
$stdout.puts "\n\e[33m Aborted.\e[0m"
|
|
74
217
|
@history.pop # Remove the user message that never got a response
|
|
218
|
+
log_interactive_turn
|
|
75
219
|
next
|
|
76
220
|
end
|
|
77
221
|
|
|
@@ -79,6 +223,11 @@ module ConsoleAgent
|
|
|
79
223
|
code = @executor.display_response(result.text)
|
|
80
224
|
display_usage(result, show_session: true)
|
|
81
225
|
|
|
226
|
+
# Save after response is displayed so viewer shows progress before Execute prompt
|
|
227
|
+
log_interactive_turn
|
|
228
|
+
|
|
229
|
+
# Add tool call/result messages so the LLM remembers what it learned
|
|
230
|
+
@history.concat(tool_messages) if tool_messages && !tool_messages.empty?
|
|
82
231
|
@history << { role: :assistant, content: result.text }
|
|
83
232
|
|
|
84
233
|
if code && !code.strip.empty?
|
|
@@ -88,6 +237,13 @@ module ConsoleAgent
|
|
|
88
237
|
exec_result = @executor.confirm_and_execute(code)
|
|
89
238
|
end
|
|
90
239
|
|
|
240
|
+
unless @executor.last_cancelled?
|
|
241
|
+
@last_interactive_code = code
|
|
242
|
+
@last_interactive_output = @executor.last_output
|
|
243
|
+
@last_interactive_result = exec_result ? exec_result.inspect : nil
|
|
244
|
+
@last_interactive_executed = true
|
|
245
|
+
end
|
|
246
|
+
|
|
91
247
|
if @executor.last_cancelled?
|
|
92
248
|
@history << { role: :user, content: "User declined to execute the code." }
|
|
93
249
|
else
|
|
@@ -110,21 +266,28 @@ module ConsoleAgent
|
|
|
110
266
|
end
|
|
111
267
|
end
|
|
112
268
|
end
|
|
269
|
+
|
|
270
|
+
# Update with the AI response, tokens, and any execution results
|
|
271
|
+
log_interactive_turn
|
|
113
272
|
end
|
|
114
273
|
|
|
115
|
-
|
|
116
|
-
|
|
274
|
+
$stdout = @interactive_old_stdout
|
|
275
|
+
@executor.on_prompt = nil
|
|
276
|
+
finish_interactive_session
|
|
277
|
+
display_exit_info
|
|
117
278
|
rescue Interrupt
|
|
118
279
|
# Ctrl-C during Readline input — exit cleanly
|
|
280
|
+
$stdout = @interactive_old_stdout if @interactive_old_stdout
|
|
281
|
+
@executor.on_prompt = nil
|
|
119
282
|
$stdout.puts
|
|
120
|
-
|
|
121
|
-
|
|
283
|
+
finish_interactive_session
|
|
284
|
+
display_exit_info
|
|
122
285
|
rescue => e
|
|
286
|
+
$stdout = @interactive_old_stdout if @interactive_old_stdout
|
|
287
|
+
@executor.on_prompt = nil
|
|
123
288
|
$stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
|
|
124
289
|
end
|
|
125
290
|
|
|
126
|
-
private
|
|
127
|
-
|
|
128
291
|
def provider
|
|
129
292
|
@provider ||= Providers.build
|
|
130
293
|
end
|
|
@@ -141,25 +304,22 @@ module ConsoleAgent
|
|
|
141
304
|
ConsoleAgent.configuration.validate!
|
|
142
305
|
|
|
143
306
|
messages = if conversation
|
|
144
|
-
conversation.
|
|
307
|
+
conversation.dup
|
|
145
308
|
else
|
|
146
309
|
[{ role: :user, content: query }]
|
|
147
310
|
end
|
|
148
311
|
|
|
149
|
-
|
|
150
|
-
send_query_with_tools(messages)
|
|
151
|
-
else
|
|
152
|
-
provider.chat(messages, system_prompt: context)
|
|
153
|
-
end
|
|
312
|
+
send_query_with_tools(messages)
|
|
154
313
|
end
|
|
155
314
|
|
|
156
315
|
def send_query_with_tools(messages)
|
|
157
316
|
require 'console_agent/tools/registry'
|
|
158
|
-
tools = Tools::Registry.new
|
|
317
|
+
tools = Tools::Registry.new(executor: @executor)
|
|
159
318
|
max_rounds = ConsoleAgent.configuration.max_tool_rounds
|
|
160
319
|
total_input = 0
|
|
161
320
|
total_output = 0
|
|
162
321
|
result = nil
|
|
322
|
+
new_messages = [] # Track messages added during tool use
|
|
163
323
|
|
|
164
324
|
exhausted = false
|
|
165
325
|
|
|
@@ -180,12 +340,14 @@ module ConsoleAgent
|
|
|
180
340
|
end
|
|
181
341
|
|
|
182
342
|
# Add assistant message with tool calls to conversation
|
|
183
|
-
|
|
343
|
+
assistant_msg = provider.format_assistant_message(result)
|
|
344
|
+
messages << assistant_msg
|
|
345
|
+
new_messages << assistant_msg
|
|
184
346
|
|
|
185
347
|
# Execute each tool and show progress
|
|
186
348
|
result.tool_calls.each do |tc|
|
|
187
|
-
# ask_user
|
|
188
|
-
if tc[:name] == 'ask_user'
|
|
349
|
+
# ask_user and execute_plan handle their own display
|
|
350
|
+
if tc[:name] == 'ask_user' || tc[:name] == 'execute_plan'
|
|
189
351
|
tool_result = tools.execute(tc[:name], tc[:arguments])
|
|
190
352
|
else
|
|
191
353
|
args_display = format_tool_args(tc[:name], tc[:arguments])
|
|
@@ -202,7 +364,9 @@ module ConsoleAgent
|
|
|
202
364
|
$stderr.puts "\e[35m[debug tool result] #{tool_result}\e[0m"
|
|
203
365
|
end
|
|
204
366
|
|
|
205
|
-
|
|
367
|
+
tool_msg = provider.format_tool_result(tc[:id], tool_result)
|
|
368
|
+
messages << tool_msg
|
|
369
|
+
new_messages << tool_msg
|
|
206
370
|
end
|
|
207
371
|
|
|
208
372
|
exhausted = true if round == max_rounds - 1
|
|
@@ -217,12 +381,13 @@ module ConsoleAgent
|
|
|
217
381
|
total_output += result.output_tokens || 0
|
|
218
382
|
end
|
|
219
383
|
|
|
220
|
-
Providers::ChatResult.new(
|
|
384
|
+
final_result = Providers::ChatResult.new(
|
|
221
385
|
text: result ? result.text : '',
|
|
222
386
|
input_tokens: total_input,
|
|
223
387
|
output_tokens: total_output,
|
|
224
388
|
stop_reason: result ? result.stop_reason : :end_turn
|
|
225
389
|
)
|
|
390
|
+
[final_result, new_messages]
|
|
226
391
|
end
|
|
227
392
|
|
|
228
393
|
def format_tool_args(name, args)
|
|
@@ -246,6 +411,9 @@ module ConsoleAgent
|
|
|
246
411
|
"(\"#{args['name']}\")"
|
|
247
412
|
when 'recall_memories'
|
|
248
413
|
args['query'] ? "(\"#{args['query']}\")" : ''
|
|
414
|
+
when 'execute_plan'
|
|
415
|
+
steps = args['steps']
|
|
416
|
+
steps ? "(#{steps.length} steps)" : ''
|
|
249
417
|
else
|
|
250
418
|
''
|
|
251
419
|
end
|
|
@@ -300,6 +468,9 @@ module ConsoleAgent
|
|
|
300
468
|
when 'recall_memories'
|
|
301
469
|
chunks = result.split("\n\n")
|
|
302
470
|
chunks.length > 1 ? "#{chunks.length} memories found" : truncate(result, 80)
|
|
471
|
+
when 'execute_plan'
|
|
472
|
+
steps_done = result.scan(/^Step \d+/).length
|
|
473
|
+
steps_done > 0 ? "#{steps_done} steps executed" : truncate(result, 80)
|
|
303
474
|
else
|
|
304
475
|
truncate(result, 80)
|
|
305
476
|
end
|
|
@@ -333,10 +504,104 @@ module ConsoleAgent
|
|
|
333
504
|
$stdout.puts line
|
|
334
505
|
end
|
|
335
506
|
|
|
507
|
+
def with_console_capture(capture_io)
|
|
508
|
+
old_stdout = $stdout
|
|
509
|
+
$stdout = TeeIO.new(old_stdout, capture_io)
|
|
510
|
+
yield
|
|
511
|
+
ensure
|
|
512
|
+
$stdout = old_stdout
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def log_interactive_turn
|
|
516
|
+
require 'console_agent/session_logger'
|
|
517
|
+
session_attrs = {
|
|
518
|
+
conversation: @history,
|
|
519
|
+
input_tokens: @total_input_tokens,
|
|
520
|
+
output_tokens: @total_output_tokens,
|
|
521
|
+
code_executed: @last_interactive_code,
|
|
522
|
+
code_output: @last_interactive_output,
|
|
523
|
+
code_result: @last_interactive_result,
|
|
524
|
+
executed: @last_interactive_executed,
|
|
525
|
+
console_output: @interactive_console_capture&.string
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if @interactive_session_id
|
|
529
|
+
SessionLogger.update(@interactive_session_id, session_attrs)
|
|
530
|
+
else
|
|
531
|
+
@interactive_session_id = SessionLogger.log(
|
|
532
|
+
session_attrs.merge(
|
|
533
|
+
query: @interactive_query || '(interactive session)',
|
|
534
|
+
mode: 'interactive',
|
|
535
|
+
name: @interactive_session_name
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def finish_interactive_session
|
|
542
|
+
require 'console_agent/session_logger'
|
|
543
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @interactive_start) * 1000).round
|
|
544
|
+
if @interactive_session_id
|
|
545
|
+
SessionLogger.update(@interactive_session_id,
|
|
546
|
+
conversation: @history,
|
|
547
|
+
input_tokens: @total_input_tokens,
|
|
548
|
+
output_tokens: @total_output_tokens,
|
|
549
|
+
code_executed: @last_interactive_code,
|
|
550
|
+
code_output: @last_interactive_output,
|
|
551
|
+
code_result: @last_interactive_result,
|
|
552
|
+
executed: @last_interactive_executed,
|
|
553
|
+
console_output: @interactive_console_capture&.string,
|
|
554
|
+
duration_ms: duration_ms
|
|
555
|
+
)
|
|
556
|
+
elsif @interactive_query
|
|
557
|
+
# Session was never created (e.g., only one turn that failed to log)
|
|
558
|
+
log_session(
|
|
559
|
+
query: @interactive_query,
|
|
560
|
+
conversation: @history,
|
|
561
|
+
mode: 'interactive',
|
|
562
|
+
code_executed: @last_interactive_code,
|
|
563
|
+
code_output: @last_interactive_output,
|
|
564
|
+
code_result: @last_interactive_result,
|
|
565
|
+
executed: @last_interactive_executed,
|
|
566
|
+
console_output: @interactive_console_capture&.string,
|
|
567
|
+
start_time: @interactive_start
|
|
568
|
+
)
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def log_session(attrs)
|
|
573
|
+
require 'console_agent/session_logger'
|
|
574
|
+
start_time = attrs.delete(:start_time)
|
|
575
|
+
duration_ms = if start_time
|
|
576
|
+
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round
|
|
577
|
+
end
|
|
578
|
+
SessionLogger.log(
|
|
579
|
+
attrs.merge(
|
|
580
|
+
input_tokens: @total_input_tokens,
|
|
581
|
+
output_tokens: @total_output_tokens,
|
|
582
|
+
duration_ms: duration_ms
|
|
583
|
+
)
|
|
584
|
+
)
|
|
585
|
+
end
|
|
586
|
+
|
|
336
587
|
def display_session_summary
|
|
337
588
|
return if @total_input_tokens == 0 && @total_output_tokens == 0
|
|
338
589
|
|
|
339
590
|
$stdout.puts "\e[2m[session totals — in: #{@total_input_tokens} | out: #{@total_output_tokens} | total: #{@total_input_tokens + @total_output_tokens}]\e[0m"
|
|
340
591
|
end
|
|
592
|
+
|
|
593
|
+
def display_exit_info
|
|
594
|
+
display_session_summary
|
|
595
|
+
if @interactive_session_id
|
|
596
|
+
$stdout.puts "\e[36mSession ##{@interactive_session_id} saved.\e[0m"
|
|
597
|
+
if @interactive_session_name
|
|
598
|
+
$stdout.puts "\e[2m Resume with: ai_resume \"#{@interactive_session_name}\"\e[0m"
|
|
599
|
+
else
|
|
600
|
+
$stdout.puts "\e[2m Name it: ai_name #{@interactive_session_id}, \"descriptive_name\"\e[0m"
|
|
601
|
+
$stdout.puts "\e[2m Resume it: ai_resume #{@interactive_session_id}\e[0m"
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
$stdout.puts "\e[36mLeft ConsoleAgent interactive mode.\e[0m"
|
|
605
|
+
end
|
|
341
606
|
end
|
|
342
607
|
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
|