console_agent 0.1.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 +129 -143
- 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 +12 -5
- data/lib/console_agent/console_methods.rb +167 -4
- data/lib/console_agent/context_builder.rb +34 -125
- data/lib/console_agent/engine.rb +5 -0
- data/lib/console_agent/executor.rb +81 -1
- data/lib/console_agent/repl.rb +363 -42
- data/lib/console_agent/session_logger.rb +79 -0
- data/lib/console_agent/storage/base.rb +27 -0
- data/lib/console_agent/storage/file_storage.rb +63 -0
- data/lib/console_agent/tools/memory_tools.rb +136 -0
- data/lib/console_agent/tools/registry.rb +228 -2
- data/lib/console_agent/version.rb +1 -1
- data/lib/console_agent.rb +143 -3
- data/lib/generators/console_agent/templates/initializer.rb +14 -6
- metadata +14 -1
data/lib/console_agent/repl.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'readline'
|
|
2
|
+
|
|
1
3
|
module ConsoleAgent
|
|
2
4
|
class Repl
|
|
3
5
|
def initialize(binding_context)
|
|
@@ -9,20 +11,48 @@ module ConsoleAgent
|
|
|
9
11
|
@history = []
|
|
10
12
|
@total_input_tokens = 0
|
|
11
13
|
@total_output_tokens = 0
|
|
14
|
+
@input_history = []
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def one_shot(query)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
25
51
|
end
|
|
52
|
+
|
|
53
|
+
log_session(@_last_log_attrs.merge(console_output: console_capture.string))
|
|
54
|
+
|
|
55
|
+
exec_result
|
|
26
56
|
rescue Providers::ProviderError => e
|
|
27
57
|
$stderr.puts "\e[31mConsoleAgent Error: #{e.message}\e[0m"
|
|
28
58
|
nil
|
|
@@ -32,10 +62,25 @@ module ConsoleAgent
|
|
|
32
62
|
end
|
|
33
63
|
|
|
34
64
|
def explain(query)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
|
|
39
84
|
nil
|
|
40
85
|
rescue Providers::ProviderError => e
|
|
41
86
|
$stderr.puts "\e[31mConsoleAgent Error: #{e.message}\e[0m"
|
|
@@ -46,27 +91,143 @@ module ConsoleAgent
|
|
|
46
91
|
end
|
|
47
92
|
|
|
48
93
|
def interactive
|
|
49
|
-
|
|
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
|
+
|
|
50
132
|
@history = []
|
|
51
133
|
@total_input_tokens = 0
|
|
52
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
|
|
53
154
|
|
|
54
155
|
loop do
|
|
55
|
-
|
|
56
|
-
input
|
|
57
|
-
break if input.nil?
|
|
156
|
+
input = Readline.readline("\e[33mai> \e[0m", false)
|
|
157
|
+
break if input.nil? # Ctrl-D
|
|
58
158
|
|
|
59
159
|
input = input.strip
|
|
60
160
|
break if input.downcase == 'exit' || input.downcase == 'quit'
|
|
61
161
|
next if input.empty?
|
|
62
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
|
+
|
|
201
|
+
# Add to Readline history (avoid consecutive duplicates)
|
|
202
|
+
Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
|
|
203
|
+
|
|
204
|
+
@interactive_query ||= input
|
|
63
205
|
@history << { role: :user, content: input }
|
|
64
206
|
|
|
65
|
-
|
|
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
|
+
|
|
213
|
+
begin
|
|
214
|
+
result, tool_messages = send_query(input, conversation: @history)
|
|
215
|
+
rescue Interrupt
|
|
216
|
+
$stdout.puts "\n\e[33m Aborted.\e[0m"
|
|
217
|
+
@history.pop # Remove the user message that never got a response
|
|
218
|
+
log_interactive_turn
|
|
219
|
+
next
|
|
220
|
+
end
|
|
221
|
+
|
|
66
222
|
track_usage(result)
|
|
67
223
|
code = @executor.display_response(result.text)
|
|
68
224
|
display_usage(result, show_session: true)
|
|
69
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?
|
|
70
231
|
@history << { role: :assistant, content: result.text }
|
|
71
232
|
|
|
72
233
|
if code && !code.strip.empty?
|
|
@@ -76,26 +237,57 @@ module ConsoleAgent
|
|
|
76
237
|
exec_result = @executor.confirm_and_execute(code)
|
|
77
238
|
end
|
|
78
239
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@
|
|
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
|
+
|
|
247
|
+
if @executor.last_cancelled?
|
|
248
|
+
@history << { role: :user, content: "User declined to execute the code." }
|
|
249
|
+
else
|
|
250
|
+
output_parts = []
|
|
251
|
+
|
|
252
|
+
# Capture printed output (puts, print, etc.)
|
|
253
|
+
if @executor.last_output && !@executor.last_output.strip.empty?
|
|
254
|
+
output_parts << "Output:\n#{@executor.last_output.strip}"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Capture return value
|
|
258
|
+
if exec_result
|
|
259
|
+
output_parts << "Return value: #{exec_result.inspect}"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
unless output_parts.empty?
|
|
263
|
+
result_str = output_parts.join("\n\n")
|
|
264
|
+
result_str = result_str[0..1000] + '...' if result_str.length > 1000
|
|
265
|
+
@history << { role: :user, content: "Code was executed. #{result_str}" }
|
|
266
|
+
end
|
|
83
267
|
end
|
|
84
268
|
end
|
|
269
|
+
|
|
270
|
+
# Update with the AI response, tokens, and any execution results
|
|
271
|
+
log_interactive_turn
|
|
85
272
|
end
|
|
86
273
|
|
|
87
|
-
|
|
88
|
-
|
|
274
|
+
$stdout = @interactive_old_stdout
|
|
275
|
+
@executor.on_prompt = nil
|
|
276
|
+
finish_interactive_session
|
|
277
|
+
display_exit_info
|
|
89
278
|
rescue Interrupt
|
|
279
|
+
# Ctrl-C during Readline input — exit cleanly
|
|
280
|
+
$stdout = @interactive_old_stdout if @interactive_old_stdout
|
|
281
|
+
@executor.on_prompt = nil
|
|
90
282
|
$stdout.puts
|
|
91
|
-
|
|
92
|
-
|
|
283
|
+
finish_interactive_session
|
|
284
|
+
display_exit_info
|
|
93
285
|
rescue => e
|
|
286
|
+
$stdout = @interactive_old_stdout if @interactive_old_stdout
|
|
287
|
+
@executor.on_prompt = nil
|
|
94
288
|
$stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
|
|
95
289
|
end
|
|
96
290
|
|
|
97
|
-
private
|
|
98
|
-
|
|
99
291
|
def provider
|
|
100
292
|
@provider ||= Providers.build
|
|
101
293
|
end
|
|
@@ -112,25 +304,24 @@ module ConsoleAgent
|
|
|
112
304
|
ConsoleAgent.configuration.validate!
|
|
113
305
|
|
|
114
306
|
messages = if conversation
|
|
115
|
-
conversation.
|
|
307
|
+
conversation.dup
|
|
116
308
|
else
|
|
117
309
|
[{ role: :user, content: query }]
|
|
118
310
|
end
|
|
119
311
|
|
|
120
|
-
|
|
121
|
-
send_query_with_tools(messages)
|
|
122
|
-
else
|
|
123
|
-
provider.chat(messages, system_prompt: context)
|
|
124
|
-
end
|
|
312
|
+
send_query_with_tools(messages)
|
|
125
313
|
end
|
|
126
314
|
|
|
127
315
|
def send_query_with_tools(messages)
|
|
128
316
|
require 'console_agent/tools/registry'
|
|
129
|
-
tools = Tools::Registry.new
|
|
317
|
+
tools = Tools::Registry.new(executor: @executor)
|
|
130
318
|
max_rounds = ConsoleAgent.configuration.max_tool_rounds
|
|
131
319
|
total_input = 0
|
|
132
320
|
total_output = 0
|
|
133
321
|
result = nil
|
|
322
|
+
new_messages = [] # Track messages added during tool use
|
|
323
|
+
|
|
324
|
+
exhausted = false
|
|
134
325
|
|
|
135
326
|
max_rounds.times do |round|
|
|
136
327
|
if round == 0
|
|
@@ -149,12 +340,14 @@ module ConsoleAgent
|
|
|
149
340
|
end
|
|
150
341
|
|
|
151
342
|
# Add assistant message with tool calls to conversation
|
|
152
|
-
|
|
343
|
+
assistant_msg = provider.format_assistant_message(result)
|
|
344
|
+
messages << assistant_msg
|
|
345
|
+
new_messages << assistant_msg
|
|
153
346
|
|
|
154
347
|
# Execute each tool and show progress
|
|
155
348
|
result.tool_calls.each do |tc|
|
|
156
|
-
# ask_user
|
|
157
|
-
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'
|
|
158
351
|
tool_result = tools.execute(tc[:name], tc[:arguments])
|
|
159
352
|
else
|
|
160
353
|
args_display = format_tool_args(tc[:name], tc[:arguments])
|
|
@@ -163,23 +356,38 @@ module ConsoleAgent
|
|
|
163
356
|
tool_result = tools.execute(tc[:name], tc[:arguments])
|
|
164
357
|
|
|
165
358
|
preview = compact_tool_result(tc[:name], tool_result)
|
|
166
|
-
|
|
359
|
+
cached_tag = tools.last_cached? ? " (cached)" : ""
|
|
360
|
+
$stdout.puts "\e[2m #{preview}#{cached_tag}\e[0m"
|
|
167
361
|
end
|
|
168
362
|
|
|
169
363
|
if ConsoleAgent.configuration.debug
|
|
170
364
|
$stderr.puts "\e[35m[debug tool result] #{tool_result}\e[0m"
|
|
171
365
|
end
|
|
172
366
|
|
|
173
|
-
|
|
367
|
+
tool_msg = provider.format_tool_result(tc[:id], tool_result)
|
|
368
|
+
messages << tool_msg
|
|
369
|
+
new_messages << tool_msg
|
|
174
370
|
end
|
|
371
|
+
|
|
372
|
+
exhausted = true if round == max_rounds - 1
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# If we hit the tool round limit, force a final response without tools
|
|
376
|
+
if exhausted
|
|
377
|
+
$stdout.puts "\e[33m Hit tool round limit (#{max_rounds}). Forcing final answer. Increase with: ConsoleAgent.configure { |c| c.max_tool_rounds = 200 }\e[0m"
|
|
378
|
+
messages << { role: :user, content: "You've used all available tool rounds. Please provide your best answer now based on what you've learned so far." }
|
|
379
|
+
result = provider.chat(messages, system_prompt: context)
|
|
380
|
+
total_input += result.input_tokens || 0
|
|
381
|
+
total_output += result.output_tokens || 0
|
|
175
382
|
end
|
|
176
383
|
|
|
177
|
-
Providers::ChatResult.new(
|
|
384
|
+
final_result = Providers::ChatResult.new(
|
|
178
385
|
text: result ? result.text : '',
|
|
179
386
|
input_tokens: total_input,
|
|
180
387
|
output_tokens: total_output,
|
|
181
388
|
stop_reason: result ? result.stop_reason : :end_turn
|
|
182
389
|
)
|
|
390
|
+
[final_result, new_messages]
|
|
183
391
|
end
|
|
184
392
|
|
|
185
393
|
def format_tool_args(name, args)
|
|
@@ -197,6 +405,15 @@ module ConsoleAgent
|
|
|
197
405
|
"(\"#{args['query']}\"#{dir})"
|
|
198
406
|
when 'list_files'
|
|
199
407
|
args['directory'] ? "(\"#{args['directory']}\")" : ''
|
|
408
|
+
when 'save_memory'
|
|
409
|
+
"(\"#{args['name']}\")"
|
|
410
|
+
when 'delete_memory'
|
|
411
|
+
"(\"#{args['name']}\")"
|
|
412
|
+
when 'recall_memories'
|
|
413
|
+
args['query'] ? "(\"#{args['query']}\")" : ''
|
|
414
|
+
when 'execute_plan'
|
|
415
|
+
steps = args['steps']
|
|
416
|
+
steps ? "(#{steps.length} steps)" : ''
|
|
200
417
|
else
|
|
201
418
|
''
|
|
202
419
|
end
|
|
@@ -244,6 +461,16 @@ module ConsoleAgent
|
|
|
244
461
|
else
|
|
245
462
|
truncate(result, 80)
|
|
246
463
|
end
|
|
464
|
+
when 'save_memory'
|
|
465
|
+
(result.start_with?('Memory saved') || result.start_with?('Memory updated')) ? result : truncate(result, 80)
|
|
466
|
+
when 'delete_memory'
|
|
467
|
+
result.start_with?('Memory deleted') ? result : truncate(result, 80)
|
|
468
|
+
when 'recall_memories'
|
|
469
|
+
chunks = result.split("\n\n")
|
|
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)
|
|
247
474
|
else
|
|
248
475
|
truncate(result, 80)
|
|
249
476
|
end
|
|
@@ -277,10 +504,104 @@ module ConsoleAgent
|
|
|
277
504
|
$stdout.puts line
|
|
278
505
|
end
|
|
279
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
|
+
|
|
280
587
|
def display_session_summary
|
|
281
588
|
return if @total_input_tokens == 0 && @total_output_tokens == 0
|
|
282
589
|
|
|
283
590
|
$stdout.puts "\e[2m[session totals — in: #{@total_input_tokens} | out: #{@total_output_tokens} | total: #{@total_input_tokens + @total_output_tokens}]\e[0m"
|
|
284
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
|
|
285
606
|
end
|
|
286
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
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module ConsoleAgent
|
|
2
|
+
module Storage
|
|
3
|
+
class StorageError < StandardError; end
|
|
4
|
+
|
|
5
|
+
class Base
|
|
6
|
+
def read(key)
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def write(key, content)
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def list(pattern)
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def exists?(key)
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def delete(key)
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|