console_agent 0.5.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 +25 -0
- 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 +148 -29
- data/lib/console_agent/tools/code_tools.rb +19 -7
- data/lib/console_agent/tools/registry.rb +22 -17
- 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,6 +113,30 @@ 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
|
```
|
|
@@ -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
|
|
@@ -330,6 +386,50 @@ module ConsoleAgent
|
|
|
330
386
|
@context ||= context_builder.build
|
|
331
387
|
end
|
|
332
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
|
+
|
|
333
433
|
def send_query(query, conversation: nil)
|
|
334
434
|
ConsoleAgent.configuration.validate!
|
|
335
435
|
|
|
@@ -342,45 +442,43 @@ module ConsoleAgent
|
|
|
342
442
|
send_query_with_tools(messages)
|
|
343
443
|
end
|
|
344
444
|
|
|
345
|
-
def send_query_with_tools(messages)
|
|
445
|
+
def send_query_with_tools(messages, system_prompt: nil, tools_override: nil)
|
|
346
446
|
require 'console_agent/tools/registry'
|
|
347
|
-
tools = Tools::Registry.new(executor: @executor)
|
|
447
|
+
tools = tools_override || Tools::Registry.new(executor: @executor)
|
|
448
|
+
active_system_prompt = system_prompt || context
|
|
348
449
|
max_rounds = ConsoleAgent.configuration.max_tool_rounds
|
|
349
450
|
total_input = 0
|
|
350
451
|
total_output = 0
|
|
351
452
|
result = nil
|
|
352
453
|
new_messages = [] # Track messages added during tool use
|
|
454
|
+
last_thinking = nil
|
|
455
|
+
last_tool_names = []
|
|
353
456
|
|
|
354
457
|
exhausted = false
|
|
355
458
|
|
|
356
459
|
max_rounds.times do |round|
|
|
357
460
|
if round == 0
|
|
358
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"
|
|
359
470
|
end
|
|
360
471
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
provider.chat_with_tools(messages, tools: tools, system_prompt: context)
|
|
364
|
-
end
|
|
365
|
-
rescue Interrupt
|
|
366
|
-
redirect = prompt_for_redirect
|
|
367
|
-
if redirect
|
|
368
|
-
messages << { role: :user, content: redirect }
|
|
369
|
-
new_messages << messages.last
|
|
370
|
-
next
|
|
371
|
-
else
|
|
372
|
-
raise
|
|
373
|
-
end
|
|
472
|
+
result = with_escape_monitoring do
|
|
473
|
+
provider.chat_with_tools(messages, tools: tools, system_prompt: active_system_prompt)
|
|
374
474
|
end
|
|
375
475
|
total_input += result.input_tokens || 0
|
|
376
476
|
total_output += result.output_tokens || 0
|
|
377
477
|
|
|
378
478
|
break unless result.tool_use?
|
|
379
479
|
|
|
380
|
-
#
|
|
381
|
-
|
|
382
|
-
$stdout.puts "\e[2m #{result.text.strip}\e[0m"
|
|
383
|
-
end
|
|
480
|
+
# Buffer thinking text for display before next LLM call
|
|
481
|
+
last_thinking = (result.text && !result.text.strip.empty?) ? result.text.strip : nil
|
|
384
482
|
|
|
385
483
|
# Add assistant message with tool calls to conversation
|
|
386
484
|
assistant_msg = provider.format_assistant_message(result)
|
|
@@ -388,6 +486,7 @@ module ConsoleAgent
|
|
|
388
486
|
new_messages << assistant_msg
|
|
389
487
|
|
|
390
488
|
# Execute each tool and show progress
|
|
489
|
+
last_tool_names = result.tool_calls.map { |tc| tc[:name] }
|
|
391
490
|
result.tool_calls.each do |tc|
|
|
392
491
|
# ask_user and execute_plan handle their own display
|
|
393
492
|
if tc[:name] == 'ask_user' || tc[:name] == 'execute_plan'
|
|
@@ -419,7 +518,7 @@ module ConsoleAgent
|
|
|
419
518
|
if exhausted
|
|
420
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"
|
|
421
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." }
|
|
422
|
-
result = provider.chat(messages, system_prompt:
|
|
521
|
+
result = provider.chat(messages, system_prompt: active_system_prompt)
|
|
423
522
|
total_input += result.input_tokens || 0
|
|
424
523
|
total_output += result.output_tokens || 0
|
|
425
524
|
end
|
|
@@ -477,13 +576,29 @@ module ConsoleAgent
|
|
|
477
576
|
end
|
|
478
577
|
end
|
|
479
578
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
|
487
602
|
end
|
|
488
603
|
|
|
489
604
|
def format_tool_args(name, args)
|
|
@@ -547,8 +662,12 @@ module ConsoleAgent
|
|
|
547
662
|
lines = result.split("\n")
|
|
548
663
|
"#{lines.length} files"
|
|
549
664
|
when 'read_file'
|
|
550
|
-
|
|
551
|
-
|
|
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
|
|
552
671
|
when 'search_code'
|
|
553
672
|
if result.start_with?('Found')
|
|
554
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
|
data/lib/console_agent.rb
CHANGED