console_agent 0.4.0 → 0.6.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 +41 -1
- data/lib/console_agent/console_methods.rb +14 -0
- data/lib/console_agent/context_builder.rb +29 -18
- data/lib/console_agent/railtie.rb +1 -1
- data/lib/console_agent/repl.rb +178 -30
- data/lib/console_agent/tools/code_tools.rb +19 -7
- data/lib/console_agent/tools/registry.rb +23 -18
- data/lib/console_agent/version.rb +1 -1
- data/lib/console_agent.rb +2 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1d37fc84ac3f2d29832d282e6db1fd394308de2b967843c3a0c9748560a7025
|
|
4
|
+
data.tar.gz: 28bd6c25529dbedfa12d2353a1648304517bab933f0a4f25e91285ccaaf08e10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6451c8b62eba1159e0233be04dabbc86bcb3b6f1dcbd6cd658c8d5517a27f1ad6a20884d30d87fabcbe75d8a51d12a6dfa2d4a59ed0adb648ce56d3b81765636
|
|
7
|
+
data.tar.gz: 3448b25930e4613a4599698d98ec1c5a2d84b113a3221d98239dc0f09351455be817882b308910bfd8bf01eab00d01670c11256624bb7cd7a30745538095c2ea
|
data/README.md
CHANGED
|
@@ -59,6 +59,7 @@ The AI calls tools behind the scenes to learn your app — schema, models, assoc
|
|
|
59
59
|
| `ai "query"` | One-shot: ask, review code, confirm |
|
|
60
60
|
| `ai! "query"` | Interactive: ask and keep chatting |
|
|
61
61
|
| `ai? "query"` | Explain only, never executes |
|
|
62
|
+
| `ai_init` | Generate/update app guide for better context |
|
|
62
63
|
|
|
63
64
|
### Multi-Step Plans
|
|
64
65
|
|
|
@@ -112,12 +113,36 @@ ai> how does sharding work?
|
|
|
112
113
|
|
|
113
114
|
Next time, it already knows — no re-reading files, fewer tokens.
|
|
114
115
|
|
|
116
|
+
### Application Guide
|
|
117
|
+
|
|
118
|
+
Run `ai_init` to have the AI explore your app and generate a guide that gets loaded into every future conversation:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
irb> ai_init
|
|
122
|
+
No existing guide. Exploring the app...
|
|
123
|
+
Thinking...
|
|
124
|
+
-> list_models
|
|
125
|
+
240 models
|
|
126
|
+
-> describe_model("User")
|
|
127
|
+
119 associations, 6 validations
|
|
128
|
+
-> describe_model("Account")
|
|
129
|
+
25 associations
|
|
130
|
+
-> search_code("Sharding", dir: "config")
|
|
131
|
+
Found 36 matches
|
|
132
|
+
...
|
|
133
|
+
Guide saved to .console_agent/console_agent.md (3204 chars)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The guide is a markdown file covering your app's models, relationships, data architecture, and gotchas. Unlike memories (which require a tool call to recall), the guide is injected directly into the system prompt — so the AI starts every session already knowing your app.
|
|
137
|
+
|
|
138
|
+
Run `ai_init` again anytime to update it.
|
|
139
|
+
|
|
115
140
|
### Interactive Mode
|
|
116
141
|
|
|
117
142
|
```
|
|
118
143
|
irb> ai!
|
|
119
144
|
ConsoleAgent interactive mode. Type 'exit' to leave.
|
|
120
|
-
Auto-execute: OFF (Shift-Tab or /auto to toggle)
|
|
145
|
+
Auto-execute: OFF (Shift-Tab or /auto to toggle) | > code to run directly | /usage | /name <label>
|
|
121
146
|
|
|
122
147
|
ai> show me all tables
|
|
123
148
|
...
|
|
@@ -132,6 +157,21 @@ ai> exit
|
|
|
132
157
|
|
|
133
158
|
Toggle `/auto` to skip confirmation prompts. `/debug` shows raw API traffic. `/usage` shows token stats.
|
|
134
159
|
|
|
160
|
+
### Direct Code Execution
|
|
161
|
+
|
|
162
|
+
Prefix any input with `>` to run Ruby code directly — no LLM round-trip. The result is added to the conversation context, so the AI knows what happened:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
ai> >User.count
|
|
166
|
+
=> 8
|
|
167
|
+
ai> how many users do I have?
|
|
168
|
+
Thinking...
|
|
169
|
+
|
|
170
|
+
You have **8 users** in your database, as confirmed by the `User.count` you just ran.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Useful for quick checks, setting up variables, or giving the AI concrete data to work with.
|
|
174
|
+
|
|
135
175
|
### Sessions
|
|
136
176
|
|
|
137
177
|
Sessions are saved automatically when session logging is enabled. You can name, list, and resume them.
|
|
@@ -130,12 +130,26 @@ module ConsoleAgent
|
|
|
130
130
|
nil
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
+
def ai_init
|
|
134
|
+
require 'console_agent/context_builder'
|
|
135
|
+
require 'console_agent/providers/base'
|
|
136
|
+
require 'console_agent/executor'
|
|
137
|
+
require 'console_agent/repl'
|
|
138
|
+
|
|
139
|
+
repl = Repl.new(__console_agent_binding)
|
|
140
|
+
repl.init_guide
|
|
141
|
+
rescue => e
|
|
142
|
+
$stderr.puts "\e[31mConsoleAgent error: #{e.message}\e[0m"
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
133
146
|
def ai(query = nil)
|
|
134
147
|
if query.nil?
|
|
135
148
|
$stderr.puts "\e[33mUsage: ai \"your question here\"\e[0m"
|
|
136
149
|
$stderr.puts "\e[33m ai \"query\" - ask + confirm execution\e[0m"
|
|
137
150
|
$stderr.puts "\e[33m ai! \"query\" - enter interactive mode (or ai! with no args)\e[0m"
|
|
138
151
|
$stderr.puts "\e[33m ai? \"query\" - explain only, no execution\e[0m"
|
|
152
|
+
$stderr.puts "\e[33m ai_init - generate/update app guide for better AI context\e[0m"
|
|
139
153
|
$stderr.puts "\e[33m ai_sessions - list recent sessions\e[0m"
|
|
140
154
|
$stderr.puts "\e[33m ai_resume - resume a session by name or id\e[0m"
|
|
141
155
|
$stderr.puts "\e[33m ai_name - name a session: ai_name 42, \"my_label\"\e[0m"
|
|
@@ -15,10 +15,32 @@ module ConsoleAgent
|
|
|
15
15
|
parts = []
|
|
16
16
|
parts << smart_system_instructions
|
|
17
17
|
parts << environment_context
|
|
18
|
+
parts << guide_context
|
|
18
19
|
parts << memory_context
|
|
19
20
|
parts.compact.join("\n\n")
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
def environment_context
|
|
24
|
+
lines = ["## Environment"]
|
|
25
|
+
lines << "- Ruby #{RUBY_VERSION}"
|
|
26
|
+
lines << "- Rails #{Rails.version}" if defined?(Rails) && Rails.respond_to?(:version)
|
|
27
|
+
|
|
28
|
+
if defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
|
29
|
+
adapter = ActiveRecord::Base.connection.adapter_name rescue 'unknown'
|
|
30
|
+
lines << "- Database adapter: #{adapter}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if defined?(Bundler)
|
|
34
|
+
key_gems = %w[devise cancancan pundit sidekiq delayed_job resque
|
|
35
|
+
paperclip carrierwave activestorage shrine
|
|
36
|
+
pg mysql2 sqlite3 mongoid]
|
|
37
|
+
loaded = key_gems.select { |g| Gem.loaded_specs.key?(g) }
|
|
38
|
+
lines << "- Key gems: #{loaded.join(', ')}" unless loaded.empty?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
lines.join("\n")
|
|
42
|
+
end
|
|
43
|
+
|
|
22
44
|
private
|
|
23
45
|
|
|
24
46
|
def smart_system_instructions
|
|
@@ -67,25 +89,14 @@ module ConsoleAgent
|
|
|
67
89
|
PROMPT
|
|
68
90
|
end
|
|
69
91
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
lines << "- Rails #{Rails.version}" if defined?(Rails) && Rails.respond_to?(:version)
|
|
74
|
-
|
|
75
|
-
if defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
|
76
|
-
adapter = ActiveRecord::Base.connection.adapter_name rescue 'unknown'
|
|
77
|
-
lines << "- Database adapter: #{adapter}"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
if defined?(Bundler)
|
|
81
|
-
key_gems = %w[devise cancancan pundit sidekiq delayed_job resque
|
|
82
|
-
paperclip carrierwave activestorage shrine
|
|
83
|
-
pg mysql2 sqlite3 mongoid]
|
|
84
|
-
loaded = key_gems.select { |g| Gem.loaded_specs.key?(g) }
|
|
85
|
-
lines << "- Key gems: #{loaded.join(', ')}" unless loaded.empty?
|
|
86
|
-
end
|
|
92
|
+
def guide_context
|
|
93
|
+
content = ConsoleAgent.storage.read(ConsoleAgent::GUIDE_KEY)
|
|
94
|
+
return nil if content.nil? || content.strip.empty?
|
|
87
95
|
|
|
88
|
-
|
|
96
|
+
"## Application Guide\n\n#{content.strip}"
|
|
97
|
+
rescue => e
|
|
98
|
+
ConsoleAgent.logger.debug("ConsoleAgent: guide context failed: #{e.message}")
|
|
99
|
+
nil
|
|
89
100
|
end
|
|
90
101
|
|
|
91
102
|
def memory_context
|
|
@@ -15,7 +15,7 @@ module ConsoleAgent
|
|
|
15
15
|
|
|
16
16
|
# Welcome message
|
|
17
17
|
if $stdout.respond_to?(:tty?) && $stdout.tty?
|
|
18
|
-
$stdout.puts "\e[36m[ConsoleAgent] AI assistant loaded. Try: ai \"show me all tables\"\e[0m"
|
|
18
|
+
$stdout.puts "\e[36m[ConsoleAgent v#{ConsoleAgent::VERSION}] AI assistant loaded. Try: ai \"show me all tables\"\e[0m"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# Pre-build context in background
|
data/lib/console_agent/repl.rb
CHANGED
|
@@ -90,6 +90,62 @@ module ConsoleAgent
|
|
|
90
90
|
nil
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
+
def init_guide
|
|
94
|
+
storage = ConsoleAgent.storage
|
|
95
|
+
existing_guide = begin
|
|
96
|
+
content = storage.read(ConsoleAgent::GUIDE_KEY)
|
|
97
|
+
(content && !content.strip.empty?) ? content.strip : nil
|
|
98
|
+
rescue
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if existing_guide
|
|
103
|
+
$stdout.puts "\e[36m Existing guide found (#{existing_guide.length} chars). Will update.\e[0m"
|
|
104
|
+
else
|
|
105
|
+
$stdout.puts "\e[36m No existing guide. Exploring the app...\e[0m"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
require 'console_agent/tools/registry'
|
|
109
|
+
init_tools = Tools::Registry.new(mode: :init)
|
|
110
|
+
sys_prompt = init_system_prompt(existing_guide)
|
|
111
|
+
messages = [{ role: :user, content: "Explore this Rails application and generate the application guide." }]
|
|
112
|
+
|
|
113
|
+
# Temporarily increase timeout — init conversations are large
|
|
114
|
+
original_timeout = ConsoleAgent.configuration.timeout
|
|
115
|
+
ConsoleAgent.configuration.timeout = [original_timeout, 120].max
|
|
116
|
+
|
|
117
|
+
result, _ = send_query_with_tools(messages, system_prompt: sys_prompt, tools_override: init_tools)
|
|
118
|
+
|
|
119
|
+
guide_text = result.text.to_s.strip
|
|
120
|
+
# Strip markdown code fences if the LLM wrapped the response
|
|
121
|
+
guide_text = guide_text.sub(/\A```(?:markdown)?\s*\n?/, '').sub(/\n?```\s*\z/, '')
|
|
122
|
+
# Strip LLM preamble/thinking before the actual guide content
|
|
123
|
+
guide_text = guide_text.sub(/\A.*?(?=^#\s)/m, '') if guide_text =~ /^#\s/m
|
|
124
|
+
|
|
125
|
+
if guide_text.empty?
|
|
126
|
+
$stdout.puts "\e[33m No guide content generated.\e[0m"
|
|
127
|
+
return nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
storage.write(ConsoleAgent::GUIDE_KEY, guide_text)
|
|
131
|
+
|
|
132
|
+
path = storage.respond_to?(:root_path) ? File.join(storage.root_path, ConsoleAgent::GUIDE_KEY) : ConsoleAgent::GUIDE_KEY
|
|
133
|
+
$stdout.puts "\e[32m Guide saved to #{path} (#{guide_text.length} chars)\e[0m"
|
|
134
|
+
display_usage(result)
|
|
135
|
+
nil
|
|
136
|
+
rescue Interrupt
|
|
137
|
+
$stdout.puts "\n\e[33m Interrupted.\e[0m"
|
|
138
|
+
nil
|
|
139
|
+
rescue Providers::ProviderError => e
|
|
140
|
+
$stderr.puts "\e[31mConsoleAgent Error: #{e.message}\e[0m"
|
|
141
|
+
nil
|
|
142
|
+
rescue => e
|
|
143
|
+
$stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
|
|
144
|
+
nil
|
|
145
|
+
ensure
|
|
146
|
+
ConsoleAgent.configuration.timeout = original_timeout if original_timeout
|
|
147
|
+
end
|
|
148
|
+
|
|
93
149
|
def interactive
|
|
94
150
|
init_interactive_state
|
|
95
151
|
interactive_loop
|
|
@@ -146,7 +202,7 @@ module ConsoleAgent
|
|
|
146
202
|
name_display = @interactive_session_name ? " (#{@interactive_session_name})" : ""
|
|
147
203
|
# Write banner to real stdout (bypass TeeIO) so it doesn't accumulate on resume
|
|
148
204
|
@interactive_old_stdout.puts "\e[36mConsoleAgent interactive mode#{name_display}. Type 'exit' or 'quit' to leave.\e[0m"
|
|
149
|
-
@interactive_old_stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle) | /usage | /name <label>\e[0m"
|
|
205
|
+
@interactive_old_stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle) | > code to run directly | /usage | /name <label>\e[0m"
|
|
150
206
|
|
|
151
207
|
# Bind Shift-Tab to insert /auto command and submit
|
|
152
208
|
if Readline.respond_to?(:parse_and_bind)
|
|
@@ -199,6 +255,35 @@ module ConsoleAgent
|
|
|
199
255
|
next
|
|
200
256
|
end
|
|
201
257
|
|
|
258
|
+
# Direct code execution with ">" prefix — skip LLM entirely
|
|
259
|
+
if input.start_with?('>') && !input.start_with?('>=')
|
|
260
|
+
raw_code = input.sub(/\A>\s?/, '')
|
|
261
|
+
Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
|
|
262
|
+
@interactive_console_capture.write("ai> #{input}\n")
|
|
263
|
+
|
|
264
|
+
exec_result = @executor.execute(raw_code)
|
|
265
|
+
|
|
266
|
+
output_parts = []
|
|
267
|
+
output_parts << "Output:\n#{@executor.last_output.strip}" if @executor.last_output && !@executor.last_output.strip.empty?
|
|
268
|
+
output_parts << "Return value: #{exec_result.inspect}" if exec_result
|
|
269
|
+
|
|
270
|
+
result_str = output_parts.join("\n\n")
|
|
271
|
+
result_str = result_str[0..1000] + '...' if result_str.length > 1000
|
|
272
|
+
|
|
273
|
+
context_msg = "User directly executed code: `#{raw_code}`"
|
|
274
|
+
context_msg += "\n#{result_str}" unless output_parts.empty?
|
|
275
|
+
@history << { role: :user, content: context_msg }
|
|
276
|
+
|
|
277
|
+
@interactive_query ||= input
|
|
278
|
+
@last_interactive_code = raw_code
|
|
279
|
+
@last_interactive_output = @executor.last_output
|
|
280
|
+
@last_interactive_result = exec_result ? exec_result.inspect : nil
|
|
281
|
+
@last_interactive_executed = true
|
|
282
|
+
|
|
283
|
+
log_interactive_turn
|
|
284
|
+
next
|
|
285
|
+
end
|
|
286
|
+
|
|
202
287
|
# Add to Readline history (avoid consecutive duplicates)
|
|
203
288
|
Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
|
|
204
289
|
|
|
@@ -301,6 +386,50 @@ module ConsoleAgent
|
|
|
301
386
|
@context ||= context_builder.build
|
|
302
387
|
end
|
|
303
388
|
|
|
389
|
+
def init_system_prompt(existing_guide)
|
|
390
|
+
env = context_builder.environment_context
|
|
391
|
+
|
|
392
|
+
prompt = <<~PROMPT
|
|
393
|
+
You are a Rails application analyst. Your job is to explore this Rails app using the
|
|
394
|
+
available tools and produce a concise markdown guide that will be injected into future
|
|
395
|
+
AI assistant sessions.
|
|
396
|
+
|
|
397
|
+
#{env}
|
|
398
|
+
|
|
399
|
+
EXPLORATION STRATEGY — be efficient to avoid timeouts:
|
|
400
|
+
1. Start with list_models to see all models and their associations
|
|
401
|
+
2. Pick the 5-8 CORE models and call describe_model on those only
|
|
402
|
+
3. Call describe_table on only 3-5 key tables (skip tables whose models already told you enough)
|
|
403
|
+
4. Use search_code sparingly — only for specific patterns you suspect (sharding, STI, concerns)
|
|
404
|
+
5. Use read_file only when you need to understand a specific pattern (read small sections, not whole files)
|
|
405
|
+
6. Do NOT exhaustively describe every table or model — focus on what's important
|
|
406
|
+
|
|
407
|
+
IMPORTANT: Keep your total tool calls under 20. Prioritize breadth over depth.
|
|
408
|
+
|
|
409
|
+
Produce a markdown document with these sections:
|
|
410
|
+
- **Application Overview**: What the app does, key domain concepts
|
|
411
|
+
- **Key Models & Relationships**: Core models and how they relate
|
|
412
|
+
- **Data Architecture**: Important tables, notable columns, any partitioning/sharding
|
|
413
|
+
- **Important Patterns**: Custom concerns, service objects, key abstractions
|
|
414
|
+
- **Common Maintenance Tasks**: Typical console operations for this app
|
|
415
|
+
- **Gotchas**: Non-obvious behaviors, tricky associations, known quirks
|
|
416
|
+
|
|
417
|
+
Keep it concise — aim for 1-2 pages. Focus on what a console user needs to know.
|
|
418
|
+
Do NOT wrap the output in markdown code fences.
|
|
419
|
+
PROMPT
|
|
420
|
+
|
|
421
|
+
if existing_guide
|
|
422
|
+
prompt += <<~UPDATE
|
|
423
|
+
|
|
424
|
+
Here is the existing guide. Update and merge with any new findings:
|
|
425
|
+
|
|
426
|
+
#{existing_guide}
|
|
427
|
+
UPDATE
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
prompt.strip
|
|
431
|
+
end
|
|
432
|
+
|
|
304
433
|
def send_query(query, conversation: nil)
|
|
305
434
|
ConsoleAgent.configuration.validate!
|
|
306
435
|
|
|
@@ -313,45 +442,43 @@ module ConsoleAgent
|
|
|
313
442
|
send_query_with_tools(messages)
|
|
314
443
|
end
|
|
315
444
|
|
|
316
|
-
def send_query_with_tools(messages)
|
|
445
|
+
def send_query_with_tools(messages, system_prompt: nil, tools_override: nil)
|
|
317
446
|
require 'console_agent/tools/registry'
|
|
318
|
-
tools = Tools::Registry.new(executor: @executor)
|
|
447
|
+
tools = tools_override || Tools::Registry.new(executor: @executor)
|
|
448
|
+
active_system_prompt = system_prompt || context
|
|
319
449
|
max_rounds = ConsoleAgent.configuration.max_tool_rounds
|
|
320
450
|
total_input = 0
|
|
321
451
|
total_output = 0
|
|
322
452
|
result = nil
|
|
323
453
|
new_messages = [] # Track messages added during tool use
|
|
454
|
+
last_thinking = nil
|
|
455
|
+
last_tool_names = []
|
|
324
456
|
|
|
325
457
|
exhausted = false
|
|
326
458
|
|
|
327
459
|
max_rounds.times do |round|
|
|
328
460
|
if round == 0
|
|
329
461
|
$stdout.puts "\e[2m Thinking...\e[0m"
|
|
462
|
+
else
|
|
463
|
+
# Show buffered thinking text before the "Calling LLM" line
|
|
464
|
+
if last_thinking
|
|
465
|
+
last_thinking.split("\n").each do |line|
|
|
466
|
+
$stdout.puts "\e[2m #{line}\e[0m"
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
$stdout.puts "\e[2m #{llm_status(round, messages, total_input, last_thinking, last_tool_names)}\e[0m"
|
|
330
470
|
end
|
|
331
471
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
provider.chat_with_tools(messages, tools: tools, system_prompt: context)
|
|
335
|
-
end
|
|
336
|
-
rescue Interrupt
|
|
337
|
-
redirect = prompt_for_redirect
|
|
338
|
-
if redirect
|
|
339
|
-
messages << { role: :user, content: redirect }
|
|
340
|
-
new_messages << messages.last
|
|
341
|
-
next
|
|
342
|
-
else
|
|
343
|
-
raise
|
|
344
|
-
end
|
|
472
|
+
result = with_escape_monitoring do
|
|
473
|
+
provider.chat_with_tools(messages, tools: tools, system_prompt: active_system_prompt)
|
|
345
474
|
end
|
|
346
475
|
total_input += result.input_tokens || 0
|
|
347
476
|
total_output += result.output_tokens || 0
|
|
348
477
|
|
|
349
478
|
break unless result.tool_use?
|
|
350
479
|
|
|
351
|
-
#
|
|
352
|
-
|
|
353
|
-
$stdout.puts "\e[2m #{result.text.strip}\e[0m"
|
|
354
|
-
end
|
|
480
|
+
# Buffer thinking text for display before next LLM call
|
|
481
|
+
last_thinking = (result.text && !result.text.strip.empty?) ? result.text.strip : nil
|
|
355
482
|
|
|
356
483
|
# Add assistant message with tool calls to conversation
|
|
357
484
|
assistant_msg = provider.format_assistant_message(result)
|
|
@@ -359,6 +486,7 @@ module ConsoleAgent
|
|
|
359
486
|
new_messages << assistant_msg
|
|
360
487
|
|
|
361
488
|
# Execute each tool and show progress
|
|
489
|
+
last_tool_names = result.tool_calls.map { |tc| tc[:name] }
|
|
362
490
|
result.tool_calls.each do |tc|
|
|
363
491
|
# ask_user and execute_plan handle their own display
|
|
364
492
|
if tc[:name] == 'ask_user' || tc[:name] == 'execute_plan'
|
|
@@ -390,7 +518,7 @@ module ConsoleAgent
|
|
|
390
518
|
if exhausted
|
|
391
519
|
$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"
|
|
392
520
|
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." }
|
|
393
|
-
result = provider.chat(messages, system_prompt:
|
|
521
|
+
result = provider.chat(messages, system_prompt: active_system_prompt)
|
|
394
522
|
total_input += result.input_tokens || 0
|
|
395
523
|
total_output += result.output_tokens || 0
|
|
396
524
|
end
|
|
@@ -448,13 +576,29 @@ module ConsoleAgent
|
|
|
448
576
|
end
|
|
449
577
|
end
|
|
450
578
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
579
|
+
|
|
580
|
+
def llm_status(round, messages, tokens_so_far, last_thinking = nil, last_tool_names = [])
|
|
581
|
+
status = "Calling LLM (round #{round + 1}, #{messages.length} msgs"
|
|
582
|
+
status += ", ~#{format_tokens(tokens_so_far)} ctx" if tokens_so_far > 0
|
|
583
|
+
status += ")"
|
|
584
|
+
if !last_thinking && last_tool_names.any?
|
|
585
|
+
# Summarize tools when there's no thinking text
|
|
586
|
+
counts = last_tool_names.tally
|
|
587
|
+
summary = counts.map { |name, n| n > 1 ? "#{name} x#{n}" : name }.join(", ")
|
|
588
|
+
status += " after #{summary}"
|
|
589
|
+
end
|
|
590
|
+
status += "..."
|
|
591
|
+
status
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def format_tokens(count)
|
|
595
|
+
if count >= 1_000_000
|
|
596
|
+
"#{(count / 1_000_000.0).round(1)}M"
|
|
597
|
+
elsif count >= 1_000
|
|
598
|
+
"#{(count / 1_000.0).round(1)}K"
|
|
599
|
+
else
|
|
600
|
+
count.to_s
|
|
601
|
+
end
|
|
458
602
|
end
|
|
459
603
|
|
|
460
604
|
def format_tool_args(name, args)
|
|
@@ -518,8 +662,12 @@ module ConsoleAgent
|
|
|
518
662
|
lines = result.split("\n")
|
|
519
663
|
"#{lines.length} files"
|
|
520
664
|
when 'read_file'
|
|
521
|
-
|
|
522
|
-
|
|
665
|
+
if result =~ /^Lines (\d+)-(\d+) of (\d+):/
|
|
666
|
+
"lines #{$1}-#{$2} of #{$3}"
|
|
667
|
+
else
|
|
668
|
+
lines = result.split("\n")
|
|
669
|
+
"#{lines.length} lines"
|
|
670
|
+
end
|
|
523
671
|
when 'search_code'
|
|
524
672
|
if result.start_with?('Found')
|
|
525
673
|
result.split("\n").first
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module ConsoleAgent
|
|
2
2
|
module Tools
|
|
3
3
|
class CodeTools
|
|
4
|
-
MAX_FILE_LINES =
|
|
4
|
+
MAX_FILE_LINES = 500
|
|
5
5
|
MAX_LIST_ENTRIES = 100
|
|
6
6
|
MAX_SEARCH_RESULTS = 50
|
|
7
7
|
|
|
@@ -28,7 +28,7 @@ module ConsoleAgent
|
|
|
28
28
|
"Error listing files: #{e.message}"
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def read_file(path)
|
|
31
|
+
def read_file(path, start_line: nil, end_line: nil)
|
|
32
32
|
return "Error: path is required." if path.nil? || path.strip.empty?
|
|
33
33
|
|
|
34
34
|
root = rails_root
|
|
@@ -45,12 +45,24 @@ module ConsoleAgent
|
|
|
45
45
|
return "File '#{path}' not found." unless File.exist?(full_path)
|
|
46
46
|
return "Error: '#{path}' is a directory, not a file." if File.directory?(full_path)
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
all_lines = File.readlines(full_path)
|
|
49
|
+
total = all_lines.length
|
|
50
|
+
|
|
51
|
+
# Apply line range if specified (1-based, inclusive)
|
|
52
|
+
if start_line || end_line
|
|
53
|
+
s = [(start_line || 1).to_i, 1].max
|
|
54
|
+
e = [(end_line || total).to_i, total].min
|
|
55
|
+
return "Error: start_line (#{s}) is beyond end of file (#{total} lines)." if s > total
|
|
56
|
+
lines = all_lines[(s - 1)..(e - 1)] || []
|
|
57
|
+
offset = s - 1
|
|
58
|
+
numbered = lines.each_with_index.map { |line, i| "#{offset + i + 1}: #{line}" }
|
|
59
|
+
header = "Lines #{s}-#{[e, s + lines.length - 1].min} of #{total}:\n"
|
|
60
|
+
header + numbered.join
|
|
61
|
+
elsif total > MAX_FILE_LINES
|
|
62
|
+
numbered = all_lines.first(MAX_FILE_LINES).each_with_index.map { |line, i| "#{i + 1}: #{line}" }
|
|
63
|
+
numbered.join + "\n... truncated (#{total} total lines, showing first #{MAX_FILE_LINES}). Use start_line/end_line to read specific sections."
|
|
52
64
|
else
|
|
53
|
-
|
|
65
|
+
all_lines.each_with_index.map { |line, i| "#{i + 1}: #{line}" }.join
|
|
54
66
|
end
|
|
55
67
|
rescue => e
|
|
56
68
|
"Error reading file '#{path}': #{e.message}"
|
|
@@ -8,8 +8,9 @@ module ConsoleAgent
|
|
|
8
8
|
# Tools that should never be cached (side effects or user interaction)
|
|
9
9
|
NO_CACHE = %w[ask_user save_memory delete_memory execute_plan].freeze
|
|
10
10
|
|
|
11
|
-
def initialize(executor: nil)
|
|
11
|
+
def initialize(executor: nil, mode: :default)
|
|
12
12
|
@executor = executor
|
|
13
|
+
@mode = mode
|
|
13
14
|
@definitions = []
|
|
14
15
|
@handlers = {}
|
|
15
16
|
@cache = {}
|
|
@@ -142,15 +143,17 @@ module ConsoleAgent
|
|
|
142
143
|
|
|
143
144
|
register(
|
|
144
145
|
name: 'read_file',
|
|
145
|
-
description: 'Read the contents of a file in this Rails app.
|
|
146
|
+
description: 'Read the contents of a file in this Rails app. Returns up to 500 lines by default. Use start_line/end_line to read specific sections of large files.',
|
|
146
147
|
parameters: {
|
|
147
148
|
'type' => 'object',
|
|
148
149
|
'properties' => {
|
|
149
|
-
'path' => { 'type' => 'string', 'description' => 'Relative file path (e.g. "app/models/user.rb")' }
|
|
150
|
+
'path' => { 'type' => 'string', 'description' => 'Relative file path (e.g. "app/models/user.rb")' },
|
|
151
|
+
'start_line' => { 'type' => 'integer', 'description' => 'First line to read (1-based). Optional — omit to start from beginning.' },
|
|
152
|
+
'end_line' => { 'type' => 'integer', 'description' => 'Last line to read (1-based, inclusive). Optional — omit to read to end.' }
|
|
150
153
|
},
|
|
151
154
|
'required' => ['path']
|
|
152
155
|
},
|
|
153
|
-
handler: ->(args) { code.read_file(args['path']) }
|
|
156
|
+
handler: ->(args) { code.read_file(args['path'], start_line: args['start_line'], end_line: args['end_line']) }
|
|
154
157
|
)
|
|
155
158
|
|
|
156
159
|
register(
|
|
@@ -167,21 +170,23 @@ module ConsoleAgent
|
|
|
167
170
|
handler: ->(args) { code.search_code(args['query'], args['directory']) }
|
|
168
171
|
)
|
|
169
172
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
'
|
|
173
|
+
unless @mode == :init
|
|
174
|
+
register(
|
|
175
|
+
name: 'ask_user',
|
|
176
|
+
description: 'Ask the console user a clarifying question. Use this when you need specific information to write accurate code (e.g. which user they are, which record to target, what value to use). Do NOT generate placeholder values like YOUR_USER_ID — ask instead.',
|
|
177
|
+
parameters: {
|
|
178
|
+
'type' => 'object',
|
|
179
|
+
'properties' => {
|
|
180
|
+
'question' => { 'type' => 'string', 'description' => 'The question to ask the user' }
|
|
181
|
+
},
|
|
182
|
+
'required' => ['question']
|
|
177
183
|
},
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
handler: ->(args) { ask_user(args['question']) }
|
|
181
|
-
)
|
|
184
|
+
handler: ->(args) { ask_user(args['question']) }
|
|
185
|
+
)
|
|
182
186
|
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
register_memory_tools
|
|
188
|
+
register_execute_plan
|
|
189
|
+
end
|
|
185
190
|
end
|
|
186
191
|
|
|
187
192
|
def register_memory_tools
|
|
@@ -285,7 +290,7 @@ module ConsoleAgent
|
|
|
285
290
|
when 'a', 'auto'
|
|
286
291
|
skip_confirmations = true
|
|
287
292
|
when 'y', 'yes'
|
|
288
|
-
|
|
293
|
+
skip_confirmations = true if steps.length == 1
|
|
289
294
|
else
|
|
290
295
|
$stdout.puts "\e[33m Plan declined.\e[0m"
|
|
291
296
|
feedback = ask_feedback("What would you like changed?")
|
data/lib/console_agent.rb
CHANGED