console_agent 0.10.0 → 0.12.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/CHANGELOG.md +15 -0
- data/README.md +101 -1
- data/app/helpers/console_agent/sessions_helper.rb +14 -0
- data/app/models/console_agent/session.rb +1 -1
- data/app/views/console_agent/sessions/index.html.erb +4 -4
- data/app/views/console_agent/sessions/show.html.erb +16 -6
- data/app/views/layouts/console_agent/application.html.erb +1 -0
- data/lib/console_agent/channel/base.rb +23 -0
- data/lib/console_agent/channel/console.rb +457 -0
- data/lib/console_agent/channel/slack.rb +182 -0
- data/lib/console_agent/configuration.rb +74 -5
- data/lib/console_agent/conversation_engine.rb +1122 -0
- data/lib/console_agent/executor.rb +239 -47
- data/lib/console_agent/providers/base.rb +7 -2
- data/lib/console_agent/providers/local.rb +112 -0
- data/lib/console_agent/railtie.rb +4 -0
- data/lib/console_agent/repl.rb +26 -1291
- data/lib/console_agent/safety_guards.rb +207 -0
- data/lib/console_agent/session_logger.rb +14 -3
- data/lib/console_agent/slack_bot.rb +473 -0
- data/lib/console_agent/tools/registry.rb +48 -16
- data/lib/console_agent/version.rb +1 -1
- data/lib/console_agent.rb +17 -3
- data/lib/generators/console_agent/templates/initializer.rb +34 -1
- data/lib/tasks/console_agent.rake +7 -0
- metadata +9 -1
data/lib/console_agent/repl.rb
CHANGED
|
@@ -1,1330 +1,65 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'console_agent/channel/console'
|
|
2
|
+
require 'console_agent/conversation_engine'
|
|
2
3
|
|
|
3
4
|
module ConsoleAgent
|
|
4
5
|
class Repl
|
|
5
6
|
def initialize(binding_context)
|
|
6
7
|
@binding_context = binding_context
|
|
7
|
-
@
|
|
8
|
-
@
|
|
9
|
-
@context_builder = nil
|
|
10
|
-
@context = nil
|
|
11
|
-
@history = []
|
|
12
|
-
@total_input_tokens = 0
|
|
13
|
-
@total_output_tokens = 0
|
|
14
|
-
@token_usage = Hash.new { |h, k| h[k] = { input: 0, output: 0 } }
|
|
15
|
-
@input_history = []
|
|
8
|
+
@channel = Channel::Console.new
|
|
9
|
+
@engine = ConversationEngine.new(binding_context: binding_context, channel: @channel)
|
|
16
10
|
end
|
|
17
11
|
|
|
18
12
|
def one_shot(query)
|
|
19
|
-
|
|
20
|
-
console_capture = StringIO.new
|
|
21
|
-
exec_result = with_console_capture(console_capture) do
|
|
22
|
-
conversation = [{ role: :user, content: query }]
|
|
23
|
-
exec_result, code, executed = one_shot_round(conversation)
|
|
24
|
-
|
|
25
|
-
# Auto-retry once if execution errored
|
|
26
|
-
if executed && @executor.last_error
|
|
27
|
-
error_msg = "Code execution failed with error: #{@executor.last_error}"
|
|
28
|
-
error_msg = error_msg[0..1000] + '...' if error_msg.length > 1000
|
|
29
|
-
conversation << { role: :assistant, content: @_last_result_text }
|
|
30
|
-
conversation << { role: :user, content: error_msg }
|
|
31
|
-
|
|
32
|
-
$stdout.puts "\e[2m Attempting to fix...\e[0m"
|
|
33
|
-
exec_result, code, executed = one_shot_round(conversation)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
@_last_log_attrs = {
|
|
37
|
-
query: query,
|
|
38
|
-
conversation: conversation,
|
|
39
|
-
mode: 'one_shot',
|
|
40
|
-
code_executed: code,
|
|
41
|
-
code_output: executed ? @executor.last_output : nil,
|
|
42
|
-
code_result: executed && exec_result ? exec_result.inspect : nil,
|
|
43
|
-
executed: executed,
|
|
44
|
-
start_time: start_time
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
exec_result
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
log_session(@_last_log_attrs.merge(console_output: console_capture.string))
|
|
51
|
-
|
|
52
|
-
exec_result
|
|
53
|
-
rescue Providers::ProviderError => e
|
|
54
|
-
$stderr.puts "\e[31mConsoleAgent Error: #{e.message}\e[0m"
|
|
55
|
-
nil
|
|
56
|
-
rescue => e
|
|
57
|
-
$stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
|
|
58
|
-
nil
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Executes one LLM round: send query, display, optionally execute code.
|
|
62
|
-
# Returns [exec_result, code, executed].
|
|
63
|
-
def one_shot_round(conversation)
|
|
64
|
-
result, _ = send_query(nil, conversation: conversation)
|
|
65
|
-
track_usage(result)
|
|
66
|
-
code = @executor.display_response(result.text)
|
|
67
|
-
display_usage(result)
|
|
68
|
-
@_last_result_text = result.text
|
|
69
|
-
|
|
70
|
-
exec_result = nil
|
|
71
|
-
executed = false
|
|
72
|
-
has_code = code && !code.strip.empty?
|
|
73
|
-
|
|
74
|
-
if has_code
|
|
75
|
-
exec_result = if ConsoleAgent.configuration.auto_execute
|
|
76
|
-
@executor.execute(code)
|
|
77
|
-
else
|
|
78
|
-
@executor.confirm_and_execute(code)
|
|
79
|
-
end
|
|
80
|
-
executed = !@executor.last_cancelled?
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
[exec_result, has_code ? code : nil, executed]
|
|
13
|
+
@engine.one_shot(query)
|
|
84
14
|
end
|
|
85
15
|
|
|
86
16
|
def explain(query)
|
|
87
|
-
|
|
88
|
-
console_capture = StringIO.new
|
|
89
|
-
with_console_capture(console_capture) do
|
|
90
|
-
result, _ = send_query(query)
|
|
91
|
-
track_usage(result)
|
|
92
|
-
@executor.display_response(result.text)
|
|
93
|
-
display_usage(result)
|
|
94
|
-
|
|
95
|
-
@_last_log_attrs = {
|
|
96
|
-
query: query,
|
|
97
|
-
conversation: [{ role: :user, content: query }, { role: :assistant, content: result.text }],
|
|
98
|
-
mode: 'explain',
|
|
99
|
-
executed: false,
|
|
100
|
-
start_time: start_time
|
|
101
|
-
}
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
log_session(@_last_log_attrs.merge(console_output: console_capture.string))
|
|
105
|
-
|
|
106
|
-
nil
|
|
107
|
-
rescue Providers::ProviderError => e
|
|
108
|
-
$stderr.puts "\e[31mConsoleAgent Error: #{e.message}\e[0m"
|
|
109
|
-
nil
|
|
110
|
-
rescue => e
|
|
111
|
-
$stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
|
|
112
|
-
nil
|
|
17
|
+
@engine.explain(query)
|
|
113
18
|
end
|
|
114
19
|
|
|
115
20
|
def init_guide
|
|
116
|
-
|
|
117
|
-
existing_guide = begin
|
|
118
|
-
content = storage.read(ConsoleAgent::GUIDE_KEY)
|
|
119
|
-
(content && !content.strip.empty?) ? content.strip : nil
|
|
120
|
-
rescue
|
|
121
|
-
nil
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
if existing_guide
|
|
125
|
-
$stdout.puts "\e[36m Existing guide found (#{existing_guide.length} chars). Will update.\e[0m"
|
|
126
|
-
else
|
|
127
|
-
$stdout.puts "\e[36m No existing guide. Exploring the app...\e[0m"
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
require 'console_agent/tools/registry'
|
|
131
|
-
init_tools = Tools::Registry.new(mode: :init)
|
|
132
|
-
sys_prompt = init_system_prompt(existing_guide)
|
|
133
|
-
messages = [{ role: :user, content: "Explore this Rails application and generate the application guide." }]
|
|
134
|
-
|
|
135
|
-
# Temporarily increase timeout — init conversations are large
|
|
136
|
-
original_timeout = ConsoleAgent.configuration.timeout
|
|
137
|
-
ConsoleAgent.configuration.timeout = [original_timeout, 120].max
|
|
138
|
-
|
|
139
|
-
result, _ = send_query_with_tools(messages, system_prompt: sys_prompt, tools_override: init_tools)
|
|
140
|
-
|
|
141
|
-
guide_text = result.text.to_s.strip
|
|
142
|
-
# Strip markdown code fences if the LLM wrapped the response
|
|
143
|
-
guide_text = guide_text.sub(/\A```(?:markdown)?\s*\n?/, '').sub(/\n?```\s*\z/, '')
|
|
144
|
-
# Strip LLM preamble/thinking before the actual guide content
|
|
145
|
-
guide_text = guide_text.sub(/\A.*?(?=^#\s)/m, '') if guide_text =~ /^#\s/m
|
|
146
|
-
|
|
147
|
-
if guide_text.empty?
|
|
148
|
-
$stdout.puts "\e[33m No guide content generated.\e[0m"
|
|
149
|
-
return nil
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
storage.write(ConsoleAgent::GUIDE_KEY, guide_text)
|
|
153
|
-
|
|
154
|
-
path = storage.respond_to?(:root_path) ? File.join(storage.root_path, ConsoleAgent::GUIDE_KEY) : ConsoleAgent::GUIDE_KEY
|
|
155
|
-
$stdout.puts "\e[32m Guide saved to #{path} (#{guide_text.length} chars)\e[0m"
|
|
156
|
-
display_usage(result)
|
|
157
|
-
nil
|
|
158
|
-
rescue Interrupt
|
|
159
|
-
$stdout.puts "\n\e[33m Interrupted.\e[0m"
|
|
160
|
-
nil
|
|
161
|
-
rescue Providers::ProviderError => e
|
|
162
|
-
$stderr.puts "\e[31mConsoleAgent Error: #{e.message}\e[0m"
|
|
163
|
-
nil
|
|
164
|
-
rescue => e
|
|
165
|
-
$stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
|
|
166
|
-
nil
|
|
167
|
-
ensure
|
|
168
|
-
ConsoleAgent.configuration.timeout = original_timeout if original_timeout
|
|
21
|
+
@engine.init_guide
|
|
169
22
|
end
|
|
170
23
|
|
|
171
24
|
def interactive
|
|
172
|
-
|
|
173
|
-
interactive_loop
|
|
25
|
+
@channel.interactive_loop(@engine)
|
|
174
26
|
end
|
|
175
27
|
|
|
176
28
|
def resume(session)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
# Restore state from the previous session
|
|
180
|
-
@history = JSON.parse(session.conversation, symbolize_names: true)
|
|
181
|
-
@interactive_session_id = session.id
|
|
182
|
-
@interactive_query = session.query
|
|
183
|
-
@interactive_session_name = session.name
|
|
184
|
-
@total_input_tokens = session.input_tokens || 0
|
|
185
|
-
@total_output_tokens = session.output_tokens || 0
|
|
186
|
-
|
|
187
|
-
# Seed the capture buffer with previous output so it's preserved on save
|
|
188
|
-
@interactive_console_capture.write(session.console_output.to_s)
|
|
189
|
-
|
|
190
|
-
# Replay to the user via the real stdout (bypass TeeIO to avoid double-capture)
|
|
191
|
-
if session.console_output && !session.console_output.strip.empty?
|
|
192
|
-
@interactive_old_stdout.puts "\e[2m--- Replaying previous session output ---\e[0m"
|
|
193
|
-
@interactive_old_stdout.puts session.console_output
|
|
194
|
-
@interactive_old_stdout.puts "\e[2m--- End of previous output ---\e[0m"
|
|
195
|
-
@interactive_old_stdout.puts
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
interactive_loop
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
private
|
|
202
|
-
|
|
203
|
-
def init_interactive_state
|
|
204
|
-
@interactive_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
205
|
-
@interactive_console_capture = StringIO.new
|
|
206
|
-
@interactive_old_stdout = $stdout
|
|
207
|
-
$stdout = TeeIO.new(@interactive_old_stdout, @interactive_console_capture)
|
|
208
|
-
@executor.on_prompt = -> { log_interactive_turn }
|
|
209
|
-
|
|
210
|
-
@history = []
|
|
211
|
-
@total_input_tokens = 0
|
|
212
|
-
@total_output_tokens = 0
|
|
213
|
-
@token_usage = Hash.new { |h, k| h[k] = { input: 0, output: 0 } }
|
|
214
|
-
@interactive_query = nil
|
|
215
|
-
@interactive_session_id = nil
|
|
216
|
-
@interactive_session_name = nil
|
|
217
|
-
@last_interactive_code = nil
|
|
218
|
-
@last_interactive_output = nil
|
|
219
|
-
@last_interactive_result = nil
|
|
220
|
-
@last_interactive_executed = false
|
|
221
|
-
@compact_warned = false
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def interactive_loop
|
|
225
|
-
auto = ConsoleAgent.configuration.auto_execute
|
|
226
|
-
name_display = @interactive_session_name ? " (#{@interactive_session_name})" : ""
|
|
227
|
-
# Write banner to real stdout (bypass TeeIO) so it doesn't accumulate on resume
|
|
228
|
-
@interactive_old_stdout.puts "\e[36mConsoleAgent interactive mode#{name_display}. Type 'exit' or 'quit' to leave.\e[0m"
|
|
229
|
-
@interactive_old_stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle) | > code | /usage | /cost | /compact | /think | /name <label>\e[0m"
|
|
230
|
-
|
|
231
|
-
# Bind Shift-Tab to insert /auto command and submit
|
|
232
|
-
if Readline.respond_to?(:parse_and_bind)
|
|
233
|
-
Readline.parse_and_bind('"\e[Z": "\C-a\C-k/auto\C-m"')
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
loop do
|
|
237
|
-
input = Readline.readline("\001\e[33m\002ai> \001\e[0m\002", false)
|
|
238
|
-
break if input.nil? # Ctrl-D
|
|
239
|
-
|
|
240
|
-
input = input.strip
|
|
241
|
-
break if input.downcase == 'exit' || input.downcase == 'quit'
|
|
242
|
-
next if input.empty?
|
|
243
|
-
|
|
244
|
-
if input == '?' || input == '/'
|
|
245
|
-
display_help
|
|
246
|
-
next
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
if input == '/auto'
|
|
250
|
-
ConsoleAgent.configuration.auto_execute = !ConsoleAgent.configuration.auto_execute
|
|
251
|
-
mode = ConsoleAgent.configuration.auto_execute ? 'ON' : 'OFF'
|
|
252
|
-
@interactive_old_stdout.puts "\e[36m Auto-execute: #{mode}\e[0m"
|
|
253
|
-
next
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
if input == '/usage'
|
|
257
|
-
display_session_summary
|
|
258
|
-
next
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
if input == '/debug'
|
|
262
|
-
ConsoleAgent.configuration.debug = !ConsoleAgent.configuration.debug
|
|
263
|
-
mode = ConsoleAgent.configuration.debug ? 'ON' : 'OFF'
|
|
264
|
-
@interactive_old_stdout.puts "\e[36m Debug: #{mode}\e[0m"
|
|
265
|
-
next
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
if input == '/compact'
|
|
269
|
-
compact_history
|
|
270
|
-
next
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
if input == '/system'
|
|
274
|
-
@interactive_old_stdout.puts "\e[2m#{context}\e[0m"
|
|
275
|
-
next
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
if input == '/context'
|
|
279
|
-
display_conversation
|
|
280
|
-
next
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
if input == '/cost'
|
|
284
|
-
display_cost_summary
|
|
285
|
-
next
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
if input.start_with?('/expand')
|
|
289
|
-
expand_id = input.sub('/expand', '').strip.to_i
|
|
290
|
-
full_output = @executor.expand_output(expand_id)
|
|
291
|
-
if full_output
|
|
292
|
-
@interactive_old_stdout.puts full_output
|
|
293
|
-
else
|
|
294
|
-
@interactive_old_stdout.puts "\e[33mNo omitted output with id #{expand_id}\e[0m"
|
|
295
|
-
end
|
|
296
|
-
next
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
if input == '/think'
|
|
300
|
-
upgrade_to_thinking_model
|
|
301
|
-
next
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
if input.start_with?('/name')
|
|
305
|
-
name = input.sub('/name', '').strip
|
|
306
|
-
if name.empty?
|
|
307
|
-
if @interactive_session_name
|
|
308
|
-
@interactive_old_stdout.puts "\e[36m Session name: #{@interactive_session_name}\e[0m"
|
|
309
|
-
else
|
|
310
|
-
@interactive_old_stdout.puts "\e[33m Usage: /name <label> (e.g. /name salesforce_user_123)\e[0m"
|
|
311
|
-
end
|
|
312
|
-
else
|
|
313
|
-
@interactive_session_name = name
|
|
314
|
-
if @interactive_session_id
|
|
315
|
-
require 'console_agent/session_logger'
|
|
316
|
-
SessionLogger.update(@interactive_session_id, name: name)
|
|
317
|
-
end
|
|
318
|
-
@interactive_old_stdout.puts "\e[36m Session named: #{name}\e[0m"
|
|
319
|
-
end
|
|
320
|
-
next
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
# Direct code execution with ">" prefix — skip LLM entirely
|
|
324
|
-
if input.start_with?('>') && !input.start_with?('>=')
|
|
325
|
-
raw_code = input.sub(/\A>\s?/, '')
|
|
326
|
-
Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
|
|
327
|
-
@interactive_console_capture.write("ai> #{input}\n")
|
|
328
|
-
|
|
329
|
-
exec_result = @executor.execute(raw_code)
|
|
330
|
-
|
|
331
|
-
output_parts = []
|
|
332
|
-
output_parts << "Output:\n#{@executor.last_output.strip}" if @executor.last_output && !@executor.last_output.strip.empty?
|
|
333
|
-
output_parts << "Return value: #{exec_result.inspect}" if exec_result
|
|
334
|
-
|
|
335
|
-
result_str = output_parts.join("\n\n")
|
|
336
|
-
result_str = result_str[0..1000] + '...' if result_str.length > 1000
|
|
337
|
-
|
|
338
|
-
context_msg = "User directly executed code: `#{raw_code}`"
|
|
339
|
-
context_msg += "\n#{result_str}" unless output_parts.empty?
|
|
340
|
-
output_id = output_parts.empty? ? nil : @executor.store_output(result_str)
|
|
341
|
-
@history << { role: :user, content: context_msg, output_id: output_id }
|
|
342
|
-
|
|
343
|
-
@interactive_query ||= input
|
|
344
|
-
@last_interactive_code = raw_code
|
|
345
|
-
@last_interactive_output = @executor.last_output
|
|
346
|
-
@last_interactive_result = exec_result ? exec_result.inspect : nil
|
|
347
|
-
@last_interactive_executed = true
|
|
348
|
-
|
|
349
|
-
log_interactive_turn
|
|
350
|
-
next
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
# Add to Readline history (avoid consecutive duplicates)
|
|
354
|
-
Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
|
|
355
|
-
|
|
356
|
-
# Auto-upgrade to thinking model on "think harder" phrases
|
|
357
|
-
if input =~ /think\s*harder/i
|
|
358
|
-
upgrade_to_thinking_model
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
@interactive_query ||= input
|
|
362
|
-
@history << { role: :user, content: input }
|
|
363
|
-
|
|
364
|
-
# Log the user's prompt line to the console capture (Readline doesn't go through $stdout)
|
|
365
|
-
@interactive_console_capture.write("ai> #{input}\n")
|
|
366
|
-
|
|
367
|
-
# Save immediately so the session is visible in the admin UI while the AI thinks
|
|
368
|
-
log_interactive_turn
|
|
369
|
-
|
|
370
|
-
status = send_and_execute
|
|
371
|
-
if status == :interrupted
|
|
372
|
-
@history.pop # Remove the user message that never got a response
|
|
373
|
-
log_interactive_turn
|
|
374
|
-
next
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
# Auto-retry once when execution fails — send error back to LLM for a fix
|
|
378
|
-
if status == :error
|
|
379
|
-
$stdout.puts "\e[2m Attempting to fix...\e[0m"
|
|
380
|
-
log_interactive_turn
|
|
381
|
-
send_and_execute
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
# Update with the AI response, tokens, and any execution results
|
|
385
|
-
log_interactive_turn
|
|
386
|
-
|
|
387
|
-
warn_if_history_large
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
$stdout = @interactive_old_stdout
|
|
391
|
-
@executor.on_prompt = nil
|
|
392
|
-
finish_interactive_session
|
|
393
|
-
display_exit_info
|
|
394
|
-
rescue Interrupt
|
|
395
|
-
# Ctrl-C during Readline input — exit cleanly
|
|
396
|
-
$stdout = @interactive_old_stdout if @interactive_old_stdout
|
|
397
|
-
@executor.on_prompt = nil
|
|
398
|
-
$stdout.puts
|
|
399
|
-
finish_interactive_session
|
|
400
|
-
display_exit_info
|
|
401
|
-
rescue => e
|
|
402
|
-
$stdout = @interactive_old_stdout if @interactive_old_stdout
|
|
403
|
-
@executor.on_prompt = nil
|
|
404
|
-
$stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
# Sends conversation to LLM, displays response, executes code if present.
|
|
408
|
-
# Returns :success, :error, :cancelled, :no_code, or :interrupted.
|
|
409
|
-
def send_and_execute
|
|
410
|
-
begin
|
|
411
|
-
result, tool_messages = send_query(nil, conversation: @history)
|
|
412
|
-
rescue Providers::ProviderError => e
|
|
413
|
-
if e.message.include?("prompt is too long") && @history.length >= 6
|
|
414
|
-
$stdout.puts "\e[33m Context limit reached. Run /compact to reduce context size, then try again.\e[0m"
|
|
415
|
-
else
|
|
416
|
-
$stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
|
|
417
|
-
end
|
|
418
|
-
return :error
|
|
419
|
-
rescue Interrupt
|
|
420
|
-
$stdout.puts "\n\e[33m Aborted.\e[0m"
|
|
421
|
-
return :interrupted
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
track_usage(result)
|
|
425
|
-
code = @executor.display_response(result.text)
|
|
426
|
-
display_usage(result, show_session: true)
|
|
427
|
-
|
|
428
|
-
# Save after response is displayed so viewer shows progress before Execute prompt
|
|
429
|
-
log_interactive_turn
|
|
430
|
-
|
|
431
|
-
# Add tool call/result messages so the LLM remembers what it learned
|
|
432
|
-
@history.concat(tool_messages) if tool_messages && !tool_messages.empty?
|
|
433
|
-
@history << { role: :assistant, content: result.text }
|
|
434
|
-
|
|
435
|
-
return :no_code unless code && !code.strip.empty?
|
|
436
|
-
|
|
437
|
-
exec_result = if ConsoleAgent.configuration.auto_execute
|
|
438
|
-
@executor.execute(code)
|
|
439
|
-
else
|
|
440
|
-
@executor.confirm_and_execute(code)
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
unless @executor.last_cancelled?
|
|
444
|
-
@last_interactive_code = code
|
|
445
|
-
@last_interactive_output = @executor.last_output
|
|
446
|
-
@last_interactive_result = exec_result ? exec_result.inspect : nil
|
|
447
|
-
@last_interactive_executed = true
|
|
448
|
-
end
|
|
449
|
-
|
|
450
|
-
if @executor.last_cancelled?
|
|
451
|
-
@history << { role: :user, content: "User declined to execute the code." }
|
|
452
|
-
:cancelled
|
|
453
|
-
elsif @executor.last_error
|
|
454
|
-
error_msg = "Code execution failed with error: #{@executor.last_error}"
|
|
455
|
-
error_msg = error_msg[0..1000] + '...' if error_msg.length > 1000
|
|
456
|
-
@history << { role: :user, content: error_msg }
|
|
457
|
-
:error
|
|
458
|
-
else
|
|
459
|
-
output_parts = []
|
|
460
|
-
|
|
461
|
-
# Capture printed output (puts, print, etc.)
|
|
462
|
-
if @executor.last_output && !@executor.last_output.strip.empty?
|
|
463
|
-
output_parts << "Output:\n#{@executor.last_output.strip}"
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
# Capture return value
|
|
467
|
-
if exec_result
|
|
468
|
-
output_parts << "Return value: #{exec_result.inspect}"
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
unless output_parts.empty?
|
|
472
|
-
result_str = output_parts.join("\n\n")
|
|
473
|
-
result_str = result_str[0..1000] + '...' if result_str.length > 1000
|
|
474
|
-
output_id = @executor.store_output(result_str)
|
|
475
|
-
@history << { role: :user, content: "Code was executed. #{result_str}", output_id: output_id }
|
|
476
|
-
end
|
|
477
|
-
|
|
478
|
-
:success
|
|
479
|
-
end
|
|
480
|
-
end
|
|
481
|
-
|
|
482
|
-
def provider
|
|
483
|
-
@provider ||= Providers.build
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
def context_builder
|
|
487
|
-
@context_builder ||= ContextBuilder.new
|
|
488
|
-
end
|
|
489
|
-
|
|
490
|
-
def context
|
|
491
|
-
base = @context_base ||= context_builder.build
|
|
492
|
-
vars = binding_variable_summary
|
|
493
|
-
vars ? "#{base}\n\n#{vars}" : base
|
|
494
|
-
end
|
|
495
|
-
|
|
496
|
-
# Summarize local and instance variables from the user's console session
|
|
497
|
-
# so the LLM knows what's available to reference in generated code.
|
|
498
|
-
def binding_variable_summary
|
|
499
|
-
parts = []
|
|
500
|
-
|
|
501
|
-
locals = @binding_context.local_variables.reject { |v| v.to_s.start_with?('_') }
|
|
502
|
-
locals.first(20).each do |var|
|
|
503
|
-
val = @binding_context.local_variable_get(var) rescue nil
|
|
504
|
-
parts << "#{var} (#{val.class})"
|
|
505
|
-
end
|
|
506
|
-
|
|
507
|
-
ivars = (@binding_context.eval("instance_variables") rescue [])
|
|
508
|
-
ivars.reject { |v| v.to_s =~ /\A@_/ }.first(20).each do |var|
|
|
509
|
-
val = @binding_context.eval(var.to_s) rescue nil
|
|
510
|
-
parts << "#{var} (#{val.class})"
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
return nil if parts.empty?
|
|
514
|
-
"The user's console session has these variables available: #{parts.join(', ')}. You can reference them directly in code."
|
|
515
|
-
rescue
|
|
516
|
-
nil
|
|
517
|
-
end
|
|
518
|
-
|
|
519
|
-
def init_system_prompt(existing_guide)
|
|
520
|
-
env = context_builder.environment_context
|
|
521
|
-
|
|
522
|
-
prompt = <<~PROMPT
|
|
523
|
-
You are a Rails application analyst. Your job is to explore this Rails app using the
|
|
524
|
-
available tools and produce a concise markdown guide that will be injected into future
|
|
525
|
-
AI assistant sessions.
|
|
526
|
-
|
|
527
|
-
#{env}
|
|
528
|
-
|
|
529
|
-
EXPLORATION STRATEGY — be efficient to avoid timeouts:
|
|
530
|
-
1. Start with list_models to see all models and their associations
|
|
531
|
-
2. Pick the 5-8 CORE models and call describe_model on those only
|
|
532
|
-
3. Call describe_table on only 3-5 key tables (skip tables whose models already told you enough)
|
|
533
|
-
4. Use search_code sparingly — only for specific patterns you suspect (sharding, STI, concerns)
|
|
534
|
-
5. Use read_file only when you need to understand a specific pattern (read small sections, not whole files)
|
|
535
|
-
6. Do NOT exhaustively describe every table or model — focus on what's important
|
|
536
|
-
|
|
537
|
-
IMPORTANT: Keep your total tool calls under 20. Prioritize breadth over depth.
|
|
538
|
-
|
|
539
|
-
Produce a markdown document with these sections:
|
|
540
|
-
- **Application Overview**: What the app does, key domain concepts
|
|
541
|
-
- **Key Models & Relationships**: Core models and how they relate
|
|
542
|
-
- **Data Architecture**: Important tables, notable columns, any partitioning/sharding
|
|
543
|
-
- **Important Patterns**: Custom concerns, service objects, key abstractions
|
|
544
|
-
- **Common Maintenance Tasks**: Typical console operations for this app
|
|
545
|
-
- **Gotchas**: Non-obvious behaviors, tricky associations, known quirks
|
|
546
|
-
|
|
547
|
-
Keep it concise — aim for 1-2 pages. Focus on what a console user needs to know.
|
|
548
|
-
Do NOT wrap the output in markdown code fences.
|
|
549
|
-
PROMPT
|
|
550
|
-
|
|
551
|
-
if existing_guide
|
|
552
|
-
prompt += <<~UPDATE
|
|
553
|
-
|
|
554
|
-
Here is the existing guide. Update and merge with any new findings:
|
|
555
|
-
|
|
556
|
-
#{existing_guide}
|
|
557
|
-
UPDATE
|
|
558
|
-
end
|
|
559
|
-
|
|
560
|
-
prompt.strip
|
|
561
|
-
end
|
|
562
|
-
|
|
563
|
-
# Number of most recent execution outputs to keep in full in the conversation.
|
|
564
|
-
# Older outputs are replaced with a short reference the LLM can recall via tool.
|
|
565
|
-
RECENT_OUTPUTS_TO_KEEP = 2
|
|
566
|
-
|
|
567
|
-
def send_query(query, conversation: nil)
|
|
568
|
-
ConsoleAgent.configuration.validate!
|
|
569
|
-
|
|
570
|
-
messages = if conversation
|
|
571
|
-
conversation.dup
|
|
572
|
-
else
|
|
573
|
-
[{ role: :user, content: query }]
|
|
574
|
-
end
|
|
575
|
-
|
|
576
|
-
messages = trim_old_outputs(messages) if conversation
|
|
577
|
-
|
|
578
|
-
send_query_with_tools(messages)
|
|
579
|
-
end
|
|
580
|
-
|
|
581
|
-
def send_query_with_tools(messages, system_prompt: nil, tools_override: nil)
|
|
582
|
-
require 'console_agent/tools/registry'
|
|
583
|
-
tools = tools_override || Tools::Registry.new(executor: @executor)
|
|
584
|
-
active_system_prompt = system_prompt || context
|
|
585
|
-
max_rounds = ConsoleAgent.configuration.max_tool_rounds
|
|
586
|
-
total_input = 0
|
|
587
|
-
total_output = 0
|
|
588
|
-
result = nil
|
|
589
|
-
new_messages = [] # Track messages added during tool use
|
|
590
|
-
last_thinking = nil
|
|
591
|
-
last_tool_names = []
|
|
592
|
-
|
|
593
|
-
exhausted = false
|
|
594
|
-
|
|
595
|
-
max_rounds.times do |round|
|
|
596
|
-
if round == 0
|
|
597
|
-
$stdout.puts "\e[2m Thinking...\e[0m"
|
|
598
|
-
else
|
|
599
|
-
# Show buffered thinking text before the "Calling LLM" line
|
|
600
|
-
if last_thinking
|
|
601
|
-
last_thinking.split("\n").each do |line|
|
|
602
|
-
$stdout.puts "\e[2m #{line}\e[0m"
|
|
603
|
-
end
|
|
604
|
-
end
|
|
605
|
-
$stdout.puts "\e[2m #{llm_status(round, messages, total_input, last_thinking, last_tool_names)}\e[0m"
|
|
606
|
-
end
|
|
607
|
-
|
|
608
|
-
if ConsoleAgent.configuration.debug
|
|
609
|
-
debug_pre_call(round, messages, active_system_prompt, tools, total_input, total_output)
|
|
610
|
-
end
|
|
611
|
-
|
|
612
|
-
begin
|
|
613
|
-
result = with_escape_monitoring do
|
|
614
|
-
provider.chat_with_tools(messages, tools: tools, system_prompt: active_system_prompt)
|
|
615
|
-
end
|
|
616
|
-
rescue Providers::ProviderError => e
|
|
617
|
-
raise
|
|
618
|
-
end
|
|
619
|
-
total_input += result.input_tokens || 0
|
|
620
|
-
total_output += result.output_tokens || 0
|
|
621
|
-
|
|
622
|
-
if ConsoleAgent.configuration.debug
|
|
623
|
-
debug_post_call(round, result, @total_input_tokens + total_input, @total_output_tokens + total_output)
|
|
624
|
-
end
|
|
625
|
-
|
|
626
|
-
break unless result.tool_use?
|
|
627
|
-
|
|
628
|
-
# Buffer thinking text for display before next LLM call
|
|
629
|
-
last_thinking = (result.text && !result.text.strip.empty?) ? result.text.strip : nil
|
|
630
|
-
|
|
631
|
-
# Add assistant message with tool calls to conversation
|
|
632
|
-
assistant_msg = provider.format_assistant_message(result)
|
|
633
|
-
messages << assistant_msg
|
|
634
|
-
new_messages << assistant_msg
|
|
635
|
-
|
|
636
|
-
# Execute each tool and show progress
|
|
637
|
-
last_tool_names = result.tool_calls.map { |tc| tc[:name] }
|
|
638
|
-
result.tool_calls.each do |tc|
|
|
639
|
-
# ask_user and execute_plan handle their own display
|
|
640
|
-
if tc[:name] == 'ask_user' || tc[:name] == 'execute_plan'
|
|
641
|
-
tool_result = tools.execute(tc[:name], tc[:arguments])
|
|
642
|
-
else
|
|
643
|
-
args_display = format_tool_args(tc[:name], tc[:arguments])
|
|
644
|
-
$stdout.puts "\e[33m -> #{tc[:name]}#{args_display}\e[0m"
|
|
645
|
-
|
|
646
|
-
tool_result = tools.execute(tc[:name], tc[:arguments])
|
|
647
|
-
|
|
648
|
-
preview = compact_tool_result(tc[:name], tool_result)
|
|
649
|
-
cached_tag = tools.last_cached? ? " (cached)" : ""
|
|
650
|
-
$stdout.puts "\e[2m #{preview}#{cached_tag}\e[0m"
|
|
651
|
-
end
|
|
652
|
-
|
|
653
|
-
if ConsoleAgent.configuration.debug
|
|
654
|
-
$stderr.puts "\e[35m[debug] tool result (#{tool_result.to_s.length} chars)\e[0m"
|
|
655
|
-
end
|
|
656
|
-
|
|
657
|
-
tool_msg = provider.format_tool_result(tc[:id], tool_result)
|
|
658
|
-
# Store large tool results so they can be trimmed from older conversation turns
|
|
659
|
-
if tool_result.to_s.length > 200
|
|
660
|
-
tool_msg[:output_id] = @executor.store_output(tool_result.to_s)
|
|
661
|
-
end
|
|
662
|
-
messages << tool_msg
|
|
663
|
-
new_messages << tool_msg
|
|
664
|
-
end
|
|
665
|
-
|
|
666
|
-
exhausted = true if round == max_rounds - 1
|
|
667
|
-
end
|
|
668
|
-
|
|
669
|
-
# If we hit the tool round limit, force a final response without tools
|
|
670
|
-
if exhausted
|
|
671
|
-
$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"
|
|
672
|
-
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." }
|
|
673
|
-
result = provider.chat(messages, system_prompt: active_system_prompt)
|
|
674
|
-
total_input += result.input_tokens || 0
|
|
675
|
-
total_output += result.output_tokens || 0
|
|
676
|
-
end
|
|
677
|
-
|
|
678
|
-
final_result = Providers::ChatResult.new(
|
|
679
|
-
text: result ? result.text : '',
|
|
680
|
-
input_tokens: total_input,
|
|
681
|
-
output_tokens: total_output,
|
|
682
|
-
stop_reason: result ? result.stop_reason : :end_turn
|
|
683
|
-
)
|
|
684
|
-
[final_result, new_messages]
|
|
685
|
-
end
|
|
686
|
-
|
|
687
|
-
# Monitors stdin for Escape (or Ctrl+C, since raw mode disables signals)
|
|
688
|
-
# and raises Interrupt in the main thread when detected.
|
|
689
|
-
def with_escape_monitoring
|
|
690
|
-
require 'io/console'
|
|
691
|
-
return yield unless $stdin.respond_to?(:raw)
|
|
692
|
-
|
|
693
|
-
monitor = Thread.new do
|
|
694
|
-
Thread.current.report_on_exception = false
|
|
695
|
-
$stdin.raw do |io|
|
|
696
|
-
loop do
|
|
697
|
-
break if Thread.current[:stop]
|
|
698
|
-
ready = IO.select([io], nil, nil, 0.2)
|
|
699
|
-
next unless ready
|
|
700
|
-
|
|
701
|
-
char = io.read_nonblock(1) rescue nil
|
|
702
|
-
next unless char
|
|
703
|
-
|
|
704
|
-
if char == "\x03" # Ctrl+C (raw mode eats the signal)
|
|
705
|
-
Thread.main.raise(Interrupt)
|
|
706
|
-
break
|
|
707
|
-
elsif char == "\e"
|
|
708
|
-
# Distinguish standalone Escape from escape sequences (arrow keys, etc.)
|
|
709
|
-
seq = IO.select([io], nil, nil, 0.05)
|
|
710
|
-
if seq
|
|
711
|
-
io.read_nonblock(10) rescue nil # consume the sequence
|
|
712
|
-
else
|
|
713
|
-
Thread.main.raise(Interrupt)
|
|
714
|
-
break
|
|
715
|
-
end
|
|
716
|
-
end
|
|
717
|
-
end
|
|
718
|
-
end
|
|
719
|
-
rescue IOError, Errno::EIO, Errno::ENODEV, Errno::ENOTTY
|
|
720
|
-
# stdin is not a TTY (e.g. in tests or piped input) — silently skip
|
|
721
|
-
end
|
|
722
|
-
|
|
723
|
-
begin
|
|
724
|
-
yield
|
|
725
|
-
ensure
|
|
726
|
-
monitor[:stop] = true
|
|
727
|
-
monitor.join(1) rescue nil
|
|
728
|
-
end
|
|
729
|
-
end
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
def llm_status(round, messages, tokens_so_far, last_thinking = nil, last_tool_names = [])
|
|
733
|
-
status = "Calling LLM (round #{round + 1}, #{messages.length} msgs"
|
|
734
|
-
status += ", ~#{format_tokens(tokens_so_far)} ctx" if tokens_so_far > 0
|
|
735
|
-
status += ")"
|
|
736
|
-
if !last_thinking && last_tool_names.any?
|
|
737
|
-
# Summarize tools when there's no thinking text
|
|
738
|
-
counts = last_tool_names.tally
|
|
739
|
-
summary = counts.map { |name, n| n > 1 ? "#{name} x#{n}" : name }.join(", ")
|
|
740
|
-
status += " after #{summary}"
|
|
741
|
-
end
|
|
742
|
-
status += "..."
|
|
743
|
-
status
|
|
744
|
-
end
|
|
745
|
-
|
|
746
|
-
def debug_pre_call(round, messages, system_prompt, tools, total_input, total_output)
|
|
747
|
-
d = "\e[35m"
|
|
748
|
-
r = "\e[0m"
|
|
749
|
-
|
|
750
|
-
# Count message types
|
|
751
|
-
user_msgs = 0
|
|
752
|
-
assistant_msgs = 0
|
|
753
|
-
tool_result_msgs = 0
|
|
754
|
-
tool_use_msgs = 0
|
|
755
|
-
output_msgs = 0
|
|
756
|
-
omitted_msgs = 0
|
|
757
|
-
total_content_chars = system_prompt.to_s.length
|
|
758
|
-
|
|
759
|
-
messages.each do |msg|
|
|
760
|
-
content_str = msg[:content].is_a?(Array) ? msg[:content].to_s : msg[:content].to_s
|
|
761
|
-
total_content_chars += content_str.length
|
|
762
|
-
|
|
763
|
-
role = msg[:role].to_s
|
|
764
|
-
if role == 'tool'
|
|
765
|
-
tool_result_msgs += 1
|
|
766
|
-
elsif msg[:content].is_a?(Array)
|
|
767
|
-
# Anthropic format — check for tool_result or tool_use blocks
|
|
768
|
-
msg[:content].each do |block|
|
|
769
|
-
next unless block.is_a?(Hash)
|
|
770
|
-
if block['type'] == 'tool_result'
|
|
771
|
-
tool_result_msgs += 1
|
|
772
|
-
omitted_msgs += 1 if block['content'].to_s.include?('Output omitted')
|
|
773
|
-
elsif block['type'] == 'tool_use'
|
|
774
|
-
tool_use_msgs += 1
|
|
775
|
-
end
|
|
776
|
-
end
|
|
777
|
-
elsif role == 'user'
|
|
778
|
-
user_msgs += 1
|
|
779
|
-
if content_str.include?('Code was executed') || content_str.include?('directly executed code')
|
|
780
|
-
output_msgs += 1
|
|
781
|
-
omitted_msgs += 1 if content_str.include?('Output omitted')
|
|
782
|
-
end
|
|
783
|
-
elsif role == 'assistant'
|
|
784
|
-
assistant_msgs += 1
|
|
785
|
-
end
|
|
786
|
-
end
|
|
787
|
-
|
|
788
|
-
tool_count = tools.respond_to?(:definitions) ? tools.definitions.length : 0
|
|
789
|
-
|
|
790
|
-
$stderr.puts "#{d}[debug] ── LLM call ##{round + 1} ──#{r}"
|
|
791
|
-
$stderr.puts "#{d}[debug] system prompt: #{format_tokens(system_prompt.to_s.length)} chars#{r}"
|
|
792
|
-
$stderr.puts "#{d}[debug] messages: #{messages.length} (#{user_msgs} user, #{assistant_msgs} assistant, #{tool_result_msgs} tool results, #{tool_use_msgs} tool calls)#{r}"
|
|
793
|
-
$stderr.puts "#{d}[debug] execution outputs: #{output_msgs} (#{omitted_msgs} omitted)#{r}" if output_msgs > 0 || omitted_msgs > 0
|
|
794
|
-
$stderr.puts "#{d}[debug] tools provided: #{tool_count}#{r}"
|
|
795
|
-
$stderr.puts "#{d}[debug] est. content size: #{format_tokens(total_content_chars)} chars#{r}"
|
|
796
|
-
if total_input > 0 || total_output > 0
|
|
797
|
-
$stderr.puts "#{d}[debug] tokens so far: in: #{format_tokens(total_input)} | out: #{format_tokens(total_output)}#{r}"
|
|
798
|
-
end
|
|
799
|
-
end
|
|
800
|
-
|
|
801
|
-
def debug_post_call(round, result, total_input, total_output)
|
|
802
|
-
d = "\e[35m"
|
|
803
|
-
r = "\e[0m"
|
|
804
|
-
|
|
805
|
-
input_t = result.input_tokens || 0
|
|
806
|
-
output_t = result.output_tokens || 0
|
|
807
|
-
model = ConsoleAgent.configuration.resolved_model
|
|
808
|
-
pricing = Configuration::PRICING[model]
|
|
809
|
-
|
|
810
|
-
parts = ["in: #{format_tokens(input_t)}", "out: #{format_tokens(output_t)}"]
|
|
811
|
-
|
|
812
|
-
if pricing
|
|
813
|
-
cost = (input_t * pricing[:input]) + (output_t * pricing[:output])
|
|
814
|
-
session_cost = (total_input * pricing[:input]) + (total_output * pricing[:output])
|
|
815
|
-
parts << "~$#{'%.4f' % cost}"
|
|
816
|
-
$stderr.puts "#{d}[debug] ← response: #{parts.join(' | ')} (session: ~$#{'%.4f' % session_cost})#{r}"
|
|
817
|
-
else
|
|
818
|
-
$stderr.puts "#{d}[debug] ← response: #{parts.join(' | ')}#{r}"
|
|
819
|
-
end
|
|
820
|
-
|
|
821
|
-
if result.tool_use?
|
|
822
|
-
tool_names = result.tool_calls.map { |tc| tc[:name] }
|
|
823
|
-
$stderr.puts "#{d}[debug] tool calls: #{tool_names.join(', ')}#{r}"
|
|
824
|
-
else
|
|
825
|
-
$stderr.puts "#{d}[debug] stop reason: #{result.stop_reason}#{r}"
|
|
826
|
-
end
|
|
827
|
-
end
|
|
828
|
-
|
|
829
|
-
def format_tokens(count)
|
|
830
|
-
if count >= 1_000_000
|
|
831
|
-
"#{(count / 1_000_000.0).round(1)}M"
|
|
832
|
-
elsif count >= 1_000
|
|
833
|
-
"#{(count / 1_000.0).round(1)}K"
|
|
834
|
-
else
|
|
835
|
-
count.to_s
|
|
836
|
-
end
|
|
29
|
+
@channel.resume_interactive(@engine, session)
|
|
837
30
|
end
|
|
838
31
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
32
|
+
# Expose engine internals for specs that inspect state
|
|
33
|
+
def instance_variable_get(name)
|
|
842
34
|
case name
|
|
843
|
-
when
|
|
844
|
-
|
|
845
|
-
when
|
|
846
|
-
|
|
847
|
-
when 'read_file'
|
|
848
|
-
"(\"#{args['path']}\")"
|
|
849
|
-
when 'search_code'
|
|
850
|
-
dir = args['directory'] ? ", dir: \"#{args['directory']}\"" : ''
|
|
851
|
-
"(\"#{args['query']}\"#{dir})"
|
|
852
|
-
when 'list_files'
|
|
853
|
-
args['directory'] ? "(\"#{args['directory']}\")" : ''
|
|
854
|
-
when 'save_memory'
|
|
855
|
-
"(\"#{args['name']}\")"
|
|
856
|
-
when 'delete_memory'
|
|
857
|
-
"(\"#{args['name']}\")"
|
|
858
|
-
when 'recall_memories'
|
|
859
|
-
args['query'] ? "(\"#{args['query']}\")" : ''
|
|
860
|
-
when 'execute_plan'
|
|
861
|
-
steps = args['steps']
|
|
862
|
-
steps ? "(#{steps.length} steps)" : ''
|
|
35
|
+
when :@history
|
|
36
|
+
@engine.history
|
|
37
|
+
when :@executor
|
|
38
|
+
@engine.instance_variable_get(:@executor)
|
|
863
39
|
else
|
|
864
|
-
|
|
40
|
+
super
|
|
865
41
|
end
|
|
866
42
|
end
|
|
867
43
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
44
|
+
# Allow specs to set internal state
|
|
45
|
+
def instance_variable_set(name, value)
|
|
871
46
|
case name
|
|
872
|
-
when
|
|
873
|
-
|
|
874
|
-
if tables.length > 8
|
|
875
|
-
"#{tables.length} tables: #{tables.first(8).join(', ')}..."
|
|
876
|
-
else
|
|
877
|
-
"#{tables.length} tables: #{result}"
|
|
878
|
-
end
|
|
879
|
-
when 'list_models'
|
|
880
|
-
lines = result.split("\n")
|
|
881
|
-
if lines.length > 6
|
|
882
|
-
"#{lines.length} models: #{lines.first(6).map { |l| l.split(' ').first }.join(', ')}..."
|
|
883
|
-
else
|
|
884
|
-
"#{lines.length} models"
|
|
885
|
-
end
|
|
886
|
-
when 'describe_table'
|
|
887
|
-
col_count = result.scan(/^\s{2}\S/).length
|
|
888
|
-
"#{col_count} columns"
|
|
889
|
-
when 'describe_model'
|
|
890
|
-
parts = []
|
|
891
|
-
assoc_count = result.scan(/^\s{2}(has_many|has_one|belongs_to|has_and_belongs_to_many)/).length
|
|
892
|
-
val_count = result.scan(/^\s{2}(presence|uniqueness|format|length|numericality|inclusion|exclusion|confirmation|acceptance)/).length
|
|
893
|
-
parts << "#{assoc_count} associations" if assoc_count > 0
|
|
894
|
-
parts << "#{val_count} validations" if val_count > 0
|
|
895
|
-
parts.empty? ? truncate(result, 80) : parts.join(', ')
|
|
896
|
-
when 'list_files'
|
|
897
|
-
lines = result.split("\n")
|
|
898
|
-
"#{lines.length} files"
|
|
899
|
-
when 'read_file'
|
|
900
|
-
if result =~ /^Lines (\d+)-(\d+) of (\d+):/
|
|
901
|
-
"lines #{$1}-#{$2} of #{$3}"
|
|
902
|
-
else
|
|
903
|
-
lines = result.split("\n")
|
|
904
|
-
"#{lines.length} lines"
|
|
905
|
-
end
|
|
906
|
-
when 'search_code'
|
|
907
|
-
if result.start_with?('Found')
|
|
908
|
-
result.split("\n").first
|
|
909
|
-
elsif result.start_with?('No matches')
|
|
910
|
-
result
|
|
911
|
-
else
|
|
912
|
-
truncate(result, 80)
|
|
913
|
-
end
|
|
914
|
-
when 'save_memory'
|
|
915
|
-
(result.start_with?('Memory saved') || result.start_with?('Memory updated')) ? result : truncate(result, 80)
|
|
916
|
-
when 'delete_memory'
|
|
917
|
-
result.start_with?('Memory deleted') ? result : truncate(result, 80)
|
|
918
|
-
when 'recall_memories'
|
|
919
|
-
chunks = result.split("\n\n")
|
|
920
|
-
chunks.length > 1 ? "#{chunks.length} memories found" : truncate(result, 80)
|
|
921
|
-
when 'execute_plan'
|
|
922
|
-
steps_done = result.scan(/^Step \d+/).length
|
|
923
|
-
steps_done > 0 ? "#{steps_done} steps executed" : truncate(result, 80)
|
|
924
|
-
else
|
|
925
|
-
truncate(result, 80)
|
|
926
|
-
end
|
|
927
|
-
end
|
|
928
|
-
|
|
929
|
-
def truncate(str, max)
|
|
930
|
-
str.length > max ? str[0..max] + '...' : str
|
|
931
|
-
end
|
|
932
|
-
|
|
933
|
-
def track_usage(result)
|
|
934
|
-
@total_input_tokens += result.input_tokens || 0
|
|
935
|
-
@total_output_tokens += result.output_tokens || 0
|
|
936
|
-
|
|
937
|
-
model = ConsoleAgent.configuration.resolved_model
|
|
938
|
-
@token_usage[model][:input] += result.input_tokens || 0
|
|
939
|
-
@token_usage[model][:output] += result.output_tokens || 0
|
|
940
|
-
end
|
|
941
|
-
|
|
942
|
-
def display_usage(result, show_session: false)
|
|
943
|
-
input = result.input_tokens
|
|
944
|
-
output = result.output_tokens
|
|
945
|
-
return unless input || output
|
|
946
|
-
|
|
947
|
-
parts = []
|
|
948
|
-
parts << "in: #{input}" if input
|
|
949
|
-
parts << "out: #{output}" if output
|
|
950
|
-
parts << "total: #{result.total_tokens}"
|
|
951
|
-
|
|
952
|
-
line = "\e[2m[tokens #{parts.join(' | ')}]\e[0m"
|
|
953
|
-
|
|
954
|
-
if show_session && (@total_input_tokens + @total_output_tokens) > result.total_tokens
|
|
955
|
-
line += "\e[2m [session: in: #{@total_input_tokens} | out: #{@total_output_tokens} | total: #{@total_input_tokens + @total_output_tokens}]\e[0m"
|
|
956
|
-
end
|
|
957
|
-
|
|
958
|
-
$stdout.puts line
|
|
959
|
-
end
|
|
960
|
-
|
|
961
|
-
def with_console_capture(capture_io)
|
|
962
|
-
old_stdout = $stdout
|
|
963
|
-
$stdout = TeeIO.new(old_stdout, capture_io)
|
|
964
|
-
yield
|
|
965
|
-
ensure
|
|
966
|
-
$stdout = old_stdout
|
|
967
|
-
end
|
|
968
|
-
|
|
969
|
-
def log_interactive_turn
|
|
970
|
-
require 'console_agent/session_logger'
|
|
971
|
-
session_attrs = {
|
|
972
|
-
conversation: @history,
|
|
973
|
-
input_tokens: @total_input_tokens,
|
|
974
|
-
output_tokens: @total_output_tokens,
|
|
975
|
-
code_executed: @last_interactive_code,
|
|
976
|
-
code_output: @last_interactive_output,
|
|
977
|
-
code_result: @last_interactive_result,
|
|
978
|
-
executed: @last_interactive_executed,
|
|
979
|
-
console_output: @interactive_console_capture&.string
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
if @interactive_session_id
|
|
983
|
-
SessionLogger.update(@interactive_session_id, session_attrs)
|
|
47
|
+
when :@history
|
|
48
|
+
@engine.instance_variable_set(:@history, value)
|
|
984
49
|
else
|
|
985
|
-
|
|
986
|
-
session_attrs.merge(
|
|
987
|
-
query: @interactive_query || '(interactive session)',
|
|
988
|
-
mode: 'interactive',
|
|
989
|
-
name: @interactive_session_name
|
|
990
|
-
)
|
|
991
|
-
)
|
|
992
|
-
end
|
|
993
|
-
end
|
|
994
|
-
|
|
995
|
-
def finish_interactive_session
|
|
996
|
-
require 'console_agent/session_logger'
|
|
997
|
-
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @interactive_start) * 1000).round
|
|
998
|
-
if @interactive_session_id
|
|
999
|
-
SessionLogger.update(@interactive_session_id,
|
|
1000
|
-
conversation: @history,
|
|
1001
|
-
input_tokens: @total_input_tokens,
|
|
1002
|
-
output_tokens: @total_output_tokens,
|
|
1003
|
-
code_executed: @last_interactive_code,
|
|
1004
|
-
code_output: @last_interactive_output,
|
|
1005
|
-
code_result: @last_interactive_result,
|
|
1006
|
-
executed: @last_interactive_executed,
|
|
1007
|
-
console_output: @interactive_console_capture&.string,
|
|
1008
|
-
duration_ms: duration_ms
|
|
1009
|
-
)
|
|
1010
|
-
elsif @interactive_query
|
|
1011
|
-
# Session was never created (e.g., only one turn that failed to log)
|
|
1012
|
-
log_session(
|
|
1013
|
-
query: @interactive_query,
|
|
1014
|
-
conversation: @history,
|
|
1015
|
-
mode: 'interactive',
|
|
1016
|
-
code_executed: @last_interactive_code,
|
|
1017
|
-
code_output: @last_interactive_output,
|
|
1018
|
-
code_result: @last_interactive_result,
|
|
1019
|
-
executed: @last_interactive_executed,
|
|
1020
|
-
console_output: @interactive_console_capture&.string,
|
|
1021
|
-
start_time: @interactive_start
|
|
1022
|
-
)
|
|
1023
|
-
end
|
|
1024
|
-
end
|
|
1025
|
-
|
|
1026
|
-
def log_session(attrs)
|
|
1027
|
-
require 'console_agent/session_logger'
|
|
1028
|
-
start_time = attrs.delete(:start_time)
|
|
1029
|
-
duration_ms = if start_time
|
|
1030
|
-
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round
|
|
1031
|
-
end
|
|
1032
|
-
SessionLogger.log(
|
|
1033
|
-
attrs.merge(
|
|
1034
|
-
input_tokens: @total_input_tokens,
|
|
1035
|
-
output_tokens: @total_output_tokens,
|
|
1036
|
-
duration_ms: duration_ms
|
|
1037
|
-
)
|
|
1038
|
-
)
|
|
1039
|
-
end
|
|
1040
|
-
|
|
1041
|
-
def display_session_summary
|
|
1042
|
-
return if @total_input_tokens == 0 && @total_output_tokens == 0
|
|
1043
|
-
|
|
1044
|
-
$stdout.puts "\e[2m[session totals — in: #{@total_input_tokens} | out: #{@total_output_tokens} | total: #{@total_input_tokens + @total_output_tokens}]\e[0m"
|
|
1045
|
-
end
|
|
1046
|
-
|
|
1047
|
-
def display_cost_summary
|
|
1048
|
-
if @token_usage.empty?
|
|
1049
|
-
$stdout.puts "\e[2m No usage yet.\e[0m"
|
|
1050
|
-
return
|
|
50
|
+
super
|
|
1051
51
|
end
|
|
1052
|
-
|
|
1053
|
-
total_cost = 0.0
|
|
1054
|
-
$stdout.puts "\e[36m Cost estimate:\e[0m"
|
|
1055
|
-
|
|
1056
|
-
@token_usage.each do |model, usage|
|
|
1057
|
-
pricing = Configuration::PRICING[model]
|
|
1058
|
-
input_str = "in: #{format_tokens(usage[:input])}"
|
|
1059
|
-
output_str = "out: #{format_tokens(usage[:output])}"
|
|
1060
|
-
|
|
1061
|
-
if pricing
|
|
1062
|
-
cost = (usage[:input] * pricing[:input]) + (usage[:output] * pricing[:output])
|
|
1063
|
-
total_cost += cost
|
|
1064
|
-
$stdout.puts "\e[2m #{model}: #{input_str} #{output_str} ~$#{'%.2f' % cost}\e[0m"
|
|
1065
|
-
else
|
|
1066
|
-
$stdout.puts "\e[2m #{model}: #{input_str} #{output_str} (pricing unknown)\e[0m"
|
|
1067
|
-
end
|
|
1068
|
-
end
|
|
1069
|
-
|
|
1070
|
-
$stdout.puts "\e[36m Total: ~$#{'%.2f' % total_cost}\e[0m"
|
|
1071
52
|
end
|
|
1072
53
|
|
|
1073
|
-
|
|
1074
|
-
config = ConsoleAgent.configuration
|
|
1075
|
-
current = config.resolved_model
|
|
1076
|
-
thinking = config.resolved_thinking_model
|
|
1077
|
-
|
|
1078
|
-
if current == thinking
|
|
1079
|
-
$stdout.puts "\e[36m Already using thinking model (#{current}).\e[0m"
|
|
1080
|
-
else
|
|
1081
|
-
config.model = thinking
|
|
1082
|
-
@provider = nil
|
|
1083
|
-
$stdout.puts "\e[36m Switched to thinking model: #{thinking}\e[0m"
|
|
1084
|
-
end
|
|
1085
|
-
end
|
|
54
|
+
private
|
|
1086
55
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
56
|
+
# Expose send methods for spec compatibility
|
|
57
|
+
def send_query(query, conversation: nil)
|
|
58
|
+
@engine.send(:send_query, query, conversation: conversation)
|
|
1090
59
|
end
|
|
1091
60
|
|
|
1092
|
-
# Replace older execution outputs with short references.
|
|
1093
|
-
# Keeps the last RECENT_OUTPUTS_TO_KEEP outputs in full.
|
|
1094
61
|
def trim_old_outputs(messages)
|
|
1095
|
-
|
|
1096
|
-
output_indices = messages.each_with_index
|
|
1097
|
-
.select { |m, _| m[:output_id] }
|
|
1098
|
-
.map { |_, i| i }
|
|
1099
|
-
|
|
1100
|
-
if output_indices.length <= RECENT_OUTPUTS_TO_KEEP
|
|
1101
|
-
return messages.map { |m| m.except(:output_id) }
|
|
1102
|
-
end
|
|
1103
|
-
|
|
1104
|
-
# Indices to trim (all except the most recent N)
|
|
1105
|
-
trim_indices = output_indices[0..-(RECENT_OUTPUTS_TO_KEEP + 1)]
|
|
1106
|
-
messages.each_with_index.map do |msg, i|
|
|
1107
|
-
if trim_indices.include?(i)
|
|
1108
|
-
trim_message(msg)
|
|
1109
|
-
else
|
|
1110
|
-
msg.except(:output_id)
|
|
1111
|
-
end
|
|
1112
|
-
end
|
|
1113
|
-
end
|
|
1114
|
-
|
|
1115
|
-
# Replace the content of a message with a short reference to the stored output.
|
|
1116
|
-
# Handles both regular messages and tool result messages (Anthropic/OpenAI formats).
|
|
1117
|
-
def trim_message(msg)
|
|
1118
|
-
ref = "[Output omitted — use recall_output tool with id #{msg[:output_id]} to retrieve]"
|
|
1119
|
-
|
|
1120
|
-
if msg[:content].is_a?(Array)
|
|
1121
|
-
# Anthropic tool_result format: [{ 'type' => 'tool_result', 'tool_use_id' => '...', 'content' => '...' }]
|
|
1122
|
-
trimmed_content = msg[:content].map do |block|
|
|
1123
|
-
if block.is_a?(Hash) && block['type'] == 'tool_result'
|
|
1124
|
-
block.merge('content' => ref)
|
|
1125
|
-
else
|
|
1126
|
-
block
|
|
1127
|
-
end
|
|
1128
|
-
end
|
|
1129
|
-
{ role: msg[:role], content: trimmed_content }
|
|
1130
|
-
elsif msg[:role].to_s == 'tool'
|
|
1131
|
-
# OpenAI tool result format
|
|
1132
|
-
msg.except(:output_id).merge(content: ref)
|
|
1133
|
-
else
|
|
1134
|
-
# Regular user message (code execution result)
|
|
1135
|
-
first_line = msg[:content].to_s.lines.first&.strip || msg[:content]
|
|
1136
|
-
{ role: msg[:role], content: "#{first_line}\n#{ref}" }
|
|
1137
|
-
end
|
|
1138
|
-
end
|
|
1139
|
-
|
|
1140
|
-
def warn_if_history_large
|
|
1141
|
-
chars = @history.sum { |m| m[:content].to_s.length }
|
|
1142
|
-
|
|
1143
|
-
if chars > 50_000 && !@compact_warned
|
|
1144
|
-
@compact_warned = true
|
|
1145
|
-
$stdout.puts "\e[33m Conversation is getting large (~#{format_tokens(chars)} chars). Consider running /compact to reduce context size.\e[0m"
|
|
1146
|
-
end
|
|
1147
|
-
end
|
|
1148
|
-
|
|
1149
|
-
def compact_history
|
|
1150
|
-
if @history.length < 6
|
|
1151
|
-
$stdout.puts "\e[33m History too short to compact (#{@history.length} messages). Need at least 6.\e[0m"
|
|
1152
|
-
return
|
|
1153
|
-
end
|
|
1154
|
-
|
|
1155
|
-
before_chars = @history.sum { |m| m[:content].to_s.length }
|
|
1156
|
-
before_count = @history.length
|
|
1157
|
-
|
|
1158
|
-
# Extract successfully executed code before summarizing
|
|
1159
|
-
executed_code = extract_executed_code(@history)
|
|
1160
|
-
|
|
1161
|
-
$stdout.puts "\e[2m Compacting #{before_count} messages (~#{format_tokens(before_chars)} chars)...\e[0m"
|
|
1162
|
-
|
|
1163
|
-
system_prompt = <<~PROMPT
|
|
1164
|
-
You are a conversation summarizer. The user will provide a conversation history from a Rails console AI assistant session.
|
|
1165
|
-
|
|
1166
|
-
Produce a concise summary that captures:
|
|
1167
|
-
- What the user has been working on and their goals
|
|
1168
|
-
- Key findings and data discovered (include specific values, IDs, record counts)
|
|
1169
|
-
- Current state: what worked, what failed, where things stand
|
|
1170
|
-
- Important variable names, model names, or table names referenced
|
|
1171
|
-
|
|
1172
|
-
Do NOT include code that was executed — that will be preserved separately.
|
|
1173
|
-
Be concise but preserve all information that would be needed to continue the conversation naturally.
|
|
1174
|
-
Do NOT include any preamble — just output the summary directly.
|
|
1175
|
-
PROMPT
|
|
1176
|
-
|
|
1177
|
-
history_text = @history.map { |m| "#{m[:role]}: #{m[:content]}" }.join("\n\n")
|
|
1178
|
-
messages = [{ role: :user, content: "Summarize this conversation history:\n\n#{history_text}" }]
|
|
1179
|
-
|
|
1180
|
-
begin
|
|
1181
|
-
result = provider.chat(messages, system_prompt: system_prompt)
|
|
1182
|
-
track_usage(result)
|
|
1183
|
-
|
|
1184
|
-
summary = result.text.to_s.strip
|
|
1185
|
-
if summary.empty?
|
|
1186
|
-
$stdout.puts "\e[33m Compaction failed: empty summary returned.\e[0m"
|
|
1187
|
-
return
|
|
1188
|
-
end
|
|
1189
|
-
|
|
1190
|
-
content = "CONVERSATION SUMMARY (compacted):\n#{summary}"
|
|
1191
|
-
unless executed_code.empty?
|
|
1192
|
-
content += "\n\nCODE EXECUTED THIS SESSION (preserved for continuation):\n#{executed_code}"
|
|
1193
|
-
end
|
|
1194
|
-
|
|
1195
|
-
@history = [{ role: :user, content: content }]
|
|
1196
|
-
@compact_warned = false
|
|
1197
|
-
|
|
1198
|
-
after_chars = @history.first[:content].length
|
|
1199
|
-
$stdout.puts "\e[36m Compacted: #{before_count} messages -> 1 summary (~#{format_tokens(before_chars)} -> ~#{format_tokens(after_chars)} chars)\e[0m"
|
|
1200
|
-
summary.each_line { |line| $stdout.puts "\e[2m #{line.rstrip}\e[0m" }
|
|
1201
|
-
if !executed_code.empty?
|
|
1202
|
-
$stdout.puts "\e[2m (preserved #{executed_code.scan(/```ruby/).length} executed code block(s))\e[0m"
|
|
1203
|
-
end
|
|
1204
|
-
display_usage(result)
|
|
1205
|
-
rescue => e
|
|
1206
|
-
$stdout.puts "\e[31m Compaction failed: #{e.message}\e[0m"
|
|
1207
|
-
end
|
|
1208
|
-
end
|
|
1209
|
-
|
|
1210
|
-
# Extracts code blocks that were successfully executed from conversation history.
|
|
1211
|
-
# Looks for:
|
|
1212
|
-
# 1. Assistant messages with ```ruby blocks followed by "Code was executed." user messages
|
|
1213
|
-
# 2. execute_plan tool calls followed by results without ERROR
|
|
1214
|
-
# Skips code that failed or was declined.
|
|
1215
|
-
def extract_executed_code(history)
|
|
1216
|
-
code_blocks = []
|
|
1217
|
-
history.each_cons(2) do |msg, next_msg|
|
|
1218
|
-
# Pattern 1: Assistant ```ruby blocks with successful execution
|
|
1219
|
-
if msg[:role].to_s == 'assistant' && next_msg[:role].to_s == 'user'
|
|
1220
|
-
content = msg[:content].to_s
|
|
1221
|
-
next_content = next_msg[:content].to_s
|
|
1222
|
-
|
|
1223
|
-
if next_content.start_with?('Code was executed.')
|
|
1224
|
-
content.scan(/```ruby\s*\n(.*?)```/m).each do |match|
|
|
1225
|
-
code = match[0].strip
|
|
1226
|
-
next if code.empty?
|
|
1227
|
-
result_summary = next_content[0..200].gsub("\n", "\n# ")
|
|
1228
|
-
code_blocks << "```ruby\n#{code}\n```\n# #{result_summary}"
|
|
1229
|
-
end
|
|
1230
|
-
end
|
|
1231
|
-
end
|
|
1232
|
-
|
|
1233
|
-
# Pattern 2: execute_plan tool calls in provider-formatted messages
|
|
1234
|
-
if msg[:role].to_s == 'assistant' && msg[:content].is_a?(Array)
|
|
1235
|
-
msg[:content].each do |block|
|
|
1236
|
-
next unless block.is_a?(Hash) && block['type'] == 'tool_use' && block['name'] == 'execute_plan'
|
|
1237
|
-
input = block['input'] || {}
|
|
1238
|
-
steps = input['steps'] || []
|
|
1239
|
-
|
|
1240
|
-
# Find the matching tool_result in subsequent messages
|
|
1241
|
-
tool_id = block['id']
|
|
1242
|
-
result_msg = find_tool_result(history, tool_id)
|
|
1243
|
-
next unless result_msg
|
|
1244
|
-
|
|
1245
|
-
result_text = result_msg.to_s
|
|
1246
|
-
# Extract only steps that succeeded (no ERROR in their result)
|
|
1247
|
-
steps.each_with_index do |step, i|
|
|
1248
|
-
step_num = i + 1
|
|
1249
|
-
# Check if this specific step had an error
|
|
1250
|
-
step_section = result_text[/Step #{step_num}\b.*?(?=Step #{step_num + 1}\b|\z)/m] || ''
|
|
1251
|
-
next if step_section.include?('ERROR:')
|
|
1252
|
-
next if step_section.include?('User declined')
|
|
1253
|
-
|
|
1254
|
-
code = step['code'].to_s.strip
|
|
1255
|
-
next if code.empty?
|
|
1256
|
-
desc = step['description'] || "Step #{step_num}"
|
|
1257
|
-
code_blocks << "```ruby\n# #{desc}\n#{code}\n```"
|
|
1258
|
-
end
|
|
1259
|
-
end
|
|
1260
|
-
end
|
|
1261
|
-
end
|
|
1262
|
-
code_blocks.join("\n\n")
|
|
1263
|
-
end
|
|
1264
|
-
|
|
1265
|
-
def find_tool_result(history, tool_id)
|
|
1266
|
-
history.each do |msg|
|
|
1267
|
-
next unless msg[:content].is_a?(Array)
|
|
1268
|
-
msg[:content].each do |block|
|
|
1269
|
-
next unless block.is_a?(Hash)
|
|
1270
|
-
if block['type'] == 'tool_result' && block['tool_use_id'] == tool_id
|
|
1271
|
-
return block['content']
|
|
1272
|
-
end
|
|
1273
|
-
# OpenAI format
|
|
1274
|
-
if msg[:role].to_s == 'tool' && msg[:tool_call_id] == tool_id
|
|
1275
|
-
return msg[:content]
|
|
1276
|
-
end
|
|
1277
|
-
end
|
|
1278
|
-
end
|
|
1279
|
-
nil
|
|
1280
|
-
end
|
|
1281
|
-
|
|
1282
|
-
def display_conversation
|
|
1283
|
-
if @history.empty?
|
|
1284
|
-
@interactive_old_stdout.puts "\e[2m (no conversation history yet)\e[0m"
|
|
1285
|
-
return
|
|
1286
|
-
end
|
|
1287
|
-
|
|
1288
|
-
trimmed = trim_old_outputs(@history)
|
|
1289
|
-
@interactive_old_stdout.puts "\e[36m Conversation (#{trimmed.length} messages, as sent to LLM):\e[0m"
|
|
1290
|
-
trimmed.each_with_index do |msg, i|
|
|
1291
|
-
role = msg[:role].to_s
|
|
1292
|
-
content = msg[:content].to_s
|
|
1293
|
-
label = role == 'user' ? "\e[33m[user]\e[0m" : "\e[36m[assistant]\e[0m"
|
|
1294
|
-
@interactive_old_stdout.puts "#{label} #{content}"
|
|
1295
|
-
@interactive_old_stdout.puts if i < trimmed.length - 1
|
|
1296
|
-
end
|
|
1297
|
-
end
|
|
1298
|
-
|
|
1299
|
-
def display_help
|
|
1300
|
-
auto = ConsoleAgent.configuration.auto_execute ? 'ON' : 'OFF'
|
|
1301
|
-
@interactive_old_stdout.puts "\e[36m Commands:\e[0m"
|
|
1302
|
-
@interactive_old_stdout.puts "\e[2m /auto Toggle auto-execute (currently #{auto}) (Shift-Tab)\e[0m"
|
|
1303
|
-
@interactive_old_stdout.puts "\e[2m /think Switch to thinking model\e[0m"
|
|
1304
|
-
@interactive_old_stdout.puts "\e[2m /compact Summarize conversation to reduce context\e[0m"
|
|
1305
|
-
@interactive_old_stdout.puts "\e[2m /usage Show session token totals\e[0m"
|
|
1306
|
-
@interactive_old_stdout.puts "\e[2m /cost Show cost estimate by model\e[0m"
|
|
1307
|
-
@interactive_old_stdout.puts "\e[2m /name <lbl> Name this session for easy resume\e[0m"
|
|
1308
|
-
@interactive_old_stdout.puts "\e[2m /context Show conversation history sent to the LLM\e[0m"
|
|
1309
|
-
@interactive_old_stdout.puts "\e[2m /system Show the system prompt\e[0m"
|
|
1310
|
-
@interactive_old_stdout.puts "\e[2m /expand <id> Show full omitted output\e[0m"
|
|
1311
|
-
@interactive_old_stdout.puts "\e[2m /debug Toggle debug summaries (context stats, cost per call)\e[0m"
|
|
1312
|
-
@interactive_old_stdout.puts "\e[2m > code Execute Ruby directly (skip LLM)\e[0m"
|
|
1313
|
-
@interactive_old_stdout.puts "\e[2m exit/quit Leave interactive mode\e[0m"
|
|
1314
|
-
end
|
|
1315
|
-
|
|
1316
|
-
def display_exit_info
|
|
1317
|
-
display_session_summary
|
|
1318
|
-
if @interactive_session_id
|
|
1319
|
-
$stdout.puts "\e[36mSession ##{@interactive_session_id} saved.\e[0m"
|
|
1320
|
-
if @interactive_session_name
|
|
1321
|
-
$stdout.puts "\e[2m Resume with: ai_resume \"#{@interactive_session_name}\"\e[0m"
|
|
1322
|
-
else
|
|
1323
|
-
$stdout.puts "\e[2m Name it: ai_name #{@interactive_session_id}, \"descriptive_name\"\e[0m"
|
|
1324
|
-
$stdout.puts "\e[2m Resume it: ai_resume #{@interactive_session_id}\e[0m"
|
|
1325
|
-
end
|
|
1326
|
-
end
|
|
1327
|
-
$stdout.puts "\e[36mLeft ConsoleAgent interactive mode.\e[0m"
|
|
62
|
+
@engine.send(:trim_old_outputs, messages)
|
|
1328
63
|
end
|
|
1329
64
|
end
|
|
1330
65
|
end
|