rails_console_ai 0.25.0 → 0.27.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89e0f9e402a40af4755fe61d2029f10a90c487c1d957e007032dcdd1f54a1c82
4
- data.tar.gz: 7e68825010aed2bd0f7a07cdcd68a39d54edec2616913ab14c9a10aa972e6d4f
3
+ metadata.gz: db0c0b3b95cc349906845d4058e1eadc6be4ab67e25f32a4b8e4262e61340a87
4
+ data.tar.gz: 0ba79ae87d8975193b2b6033279dc41a8841cf71e7f015ce8f36dafa6d62d463
5
5
  SHA512:
6
- metadata.gz: f0efcfa92b22f8cd4df14a643904aba9c0e3c05ac7873de6a303f72fd6cdd4df5c457cf1fdcafb120a02784c7247663e48141500f69214ac5bbfafa945ec9ce0
7
- data.tar.gz: 2aed178645529500d0b1d72772e19307d4b42de2a0db586f928a46df2a6503d78f3693cde4222006ad0e4227ea840b4bd510a6ef5631e9395466cf9035db679a
6
+ metadata.gz: 615da46e5aa24149783309d69a3b739db1170459c1bf6d929c8ecfc875fbc0654117a7c5d15057aaf7d421660afa9959192f4112b25e722e5e224ce3de768fb0
7
+ data.tar.gz: cf807c3dfc0fae08a79013606c5e00383bc26994461c02e4135f8efb495540a0f677bff62eae01c2f396e9ce2de8ede7c82a95415b69831425447aa93361dbdb
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.26.0]
6
+
7
+ - Add sub-agent support
8
+ - Add integration tests
9
+ - Increase max conversation rounds
10
+ - Fix sub-agent model resolution
11
+ - Improve plan step failure handling
12
+
5
13
  ## [0.25.0]
6
14
 
7
15
  - Expand truncation limits
data/README.md CHANGED
@@ -105,8 +105,69 @@ Say "think harder" in any query to auto-upgrade to the thinking model for that s
105
105
  - **History compaction** — `/compact` summarizes long conversations to reduce cost and latency
106
106
  - **Output trimming** — older execution outputs are automatically replaced with references; the LLM can recall them on demand via `recall_output`, and you can `/expand <id>` to see them
107
107
  - **Debug mode** — `/debug` shows context breakdown, token counts, and per-call cost estimates before and after each LLM call
108
+ - **Sub-agents** — delegate multi-step investigations to a separate LLM context that returns only a concise summary, keeping the main conversation lean
108
109
  - **Safe mode** — configurable guards that block side effects (DB writes, HTTP mutations, email delivery) during AI code execution
109
110
 
111
+ ## Sub-Agents
112
+
113
+ Sub-agents solve the context bloat problem. When the AI needs to investigate something (find a user's shard, explore model relationships, search code), those intermediate tool calls can inflate the main conversation to 90K+ tokens, causing the LLM to cut corners. Sub-agents fork the investigation into a separate LLM conversation and return only a concise summary.
114
+
115
+ The AI decides when to use sub-agents via the `delegate_task` tool. It can target a custom agent by name or use a general-purpose investigation.
116
+
117
+ ### Custom Agents
118
+
119
+ Define agents as markdown files in `.rails_console_ai/agents/`:
120
+
121
+ ```markdown
122
+ ---
123
+ name: Find shard
124
+ description: Given a user ID, determines which database shard they are on
125
+ max_rounds: 5
126
+ tools:
127
+ - execute_code
128
+ - recall_memory
129
+ ---
130
+
131
+ You are a shard finder for a sharded Rails application.
132
+
133
+ Steps:
134
+ 1. Find the user: User.find(id)
135
+ 2. Check user.shard
136
+ 3. Report: "User {username} (ID {id}) is on shard {shard}."
137
+ ```
138
+
139
+ **Frontmatter fields:**
140
+
141
+ | Field | Required | Description |
142
+ |-------|----------|-------------|
143
+ | `name` | yes | Agent name (shown in system prompt, used with `delegate_task`) |
144
+ | `description` | yes | One-line description of what this agent does |
145
+ | `max_rounds` | no | Max tool-use rounds (default: `sub_agent_max_rounds` config, default 15) |
146
+ | `model` | no | Model override (e.g. use Haiku for simple lookups) |
147
+ | `tools` | no | Array of tool names to include (default: all sub-agent tools) |
148
+
149
+ The markdown body becomes additional system prompt instructions for the sub-agent.
150
+
151
+ ### How It Works
152
+
153
+ 1. Agent summaries appear in the AI's system prompt under `## Agents`
154
+ 2. The AI calls `delegate_task(task: "find user 56653's shard", agent: "Find shard")`
155
+ 3. A sub-agent spins up with its own context, tools, and provider
156
+ 4. It runs the investigation (up to `max_rounds` tool calls)
157
+ 5. The main conversation receives only: `"Sub-agent result: User 56653 is on shard 5."`
158
+
159
+ Sub-agents have access to read-only memory tools (`recall_memory`, `recall_memories`), code execution (`execute_code`), and all schema/code introspection tools. They cannot ask the user questions, write memories, or spawn further sub-agents.
160
+
161
+ ### Configuration
162
+
163
+ ```ruby
164
+ RailsConsoleAi.configure do |config|
165
+ config.sub_agent_max_rounds = 15 # default max rounds per sub-agent
166
+ config.sub_agent_model = nil # nil = same model as main conversation
167
+ # config.sub_agent_model = 'claude-haiku-4-5-20251001' # use a cheaper model
168
+ end
169
+ ```
170
+
110
171
  ## Safety Guards
111
172
 
112
173
  Safety guards prevent AI-generated code from causing side effects. When a guard blocks an operation, the user is prompted to re-run with safe mode disabled.
@@ -0,0 +1,126 @@
1
+ require 'yaml'
2
+
3
+ module RailsConsoleAi
4
+ class AgentLoader
5
+ AGENTS_DIR = 'agents'
6
+ BUILTIN_DIR = File.expand_path('../agents', __FILE__)
7
+
8
+ def initialize(storage = nil)
9
+ @storage = storage || RailsConsoleAi.storage
10
+ end
11
+
12
+ def load_all_agents
13
+ agents = load_builtin_agents
14
+ app_agents = load_app_agents
15
+ # App-specific agents override built-ins with the same name
16
+ app_names = app_agents.map { |a| a['name'].to_s.downcase }.to_set
17
+ agents.reject! { |a| app_names.include?(a['name'].to_s.downcase) }
18
+ agents.concat(app_agents)
19
+ agents
20
+ rescue => e
21
+ RailsConsoleAi.logger.warn("RailsConsoleAi: failed to load agents: #{e.message}")
22
+ []
23
+ end
24
+
25
+ def agent_summaries
26
+ agents = load_all_agents
27
+ return nil if agents.empty?
28
+
29
+ agents.map { |a|
30
+ "- **#{a['name']}**: #{a['description']}"
31
+ }
32
+ end
33
+
34
+ def find_agent(name)
35
+ agents = load_all_agents
36
+ agents.find { |a| a['name'].to_s.downcase == name.to_s.downcase }
37
+ end
38
+
39
+ def save_agent(name:, description:, body:, max_rounds: nil, model: nil, tools: nil)
40
+ key = agent_key(name)
41
+ existing = find_agent(name)
42
+
43
+ frontmatter = {
44
+ 'name' => name,
45
+ 'description' => description
46
+ }
47
+ frontmatter['max_rounds'] = max_rounds if max_rounds
48
+ frontmatter['model'] = model if model
49
+ frontmatter['tools'] = Array(tools) if tools && !tools.empty?
50
+
51
+ content = "---\n#{YAML.dump(frontmatter).sub("---\n", '').strip}\n---\n\n#{body}\n"
52
+ @storage.write(key, content)
53
+
54
+ path = @storage.respond_to?(:root_path) ? File.join(@storage.root_path, key) : key
55
+ if existing
56
+ "Agent updated: \"#{name}\" (#{path})"
57
+ else
58
+ "Agent created: \"#{name}\" (#{path})"
59
+ end
60
+ rescue Storage::StorageError => e
61
+ "FAILED to save agent (#{e.message})."
62
+ end
63
+
64
+ def delete_agent(name:)
65
+ key = agent_key(name)
66
+ unless @storage.exists?(key)
67
+ found = load_all_agents.find { |a| a['name'].to_s.downcase == name.to_s.downcase }
68
+ return "No agent found: \"#{name}\"" unless found
69
+ key = agent_key(found['name'])
70
+ end
71
+
72
+ agent = load_agent(key)
73
+ @storage.delete(key)
74
+ "Agent deleted: \"#{agent ? agent['name'] : name}\""
75
+ rescue Storage::StorageError => e
76
+ "FAILED to delete agent (#{e.message})."
77
+ end
78
+
79
+ private
80
+
81
+ def load_builtin_agents
82
+ return [] unless File.directory?(BUILTIN_DIR)
83
+ Dir.glob(File.join(BUILTIN_DIR, '*.md')).sort.filter_map do |path|
84
+ content = File.read(path)
85
+ agent = parse_agent(content)
86
+ agent['builtin'] = true if agent
87
+ agent
88
+ end
89
+ rescue => e
90
+ RailsConsoleAi.logger.debug("RailsConsoleAi: failed to load built-in agents: #{e.message}")
91
+ []
92
+ end
93
+
94
+ def load_app_agents
95
+ keys = @storage.list("#{AGENTS_DIR}/*.md")
96
+ keys.filter_map { |key| load_agent(key) }
97
+ rescue => e
98
+ []
99
+ end
100
+
101
+ def agent_key(name)
102
+ slug = name.downcase.strip
103
+ .gsub(/[^a-z0-9\s-]/, '')
104
+ .gsub(/[\s]+/, '-')
105
+ .gsub(/-+/, '-')
106
+ .sub(/^-/, '').sub(/-$/, '')
107
+ "#{AGENTS_DIR}/#{slug}.md"
108
+ end
109
+
110
+ def load_agent(key)
111
+ content = @storage.read(key)
112
+ return nil if content.nil? || content.strip.empty?
113
+ parse_agent(content)
114
+ rescue => e
115
+ RailsConsoleAi.logger.warn("RailsConsoleAi: failed to load agent #{key}: #{e.message}")
116
+ nil
117
+ end
118
+
119
+ def parse_agent(content)
120
+ return nil unless content =~ /\A---\s*\n(.*?\n)---\s*\n(.*)/m
121
+ frontmatter = YAML.safe_load($1, permitted_classes: [Time, Date]) || {}
122
+ body = $2.strip
123
+ frontmatter.merge('body' => body)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: Explore data
3
+ description: Run queries to investigate records, follow associations, and gather specific data
4
+ max_rounds: 20
5
+ ---
6
+
7
+ You are a data investigator for a Rails application. Your job is to run queries, check
8
+ records, follow associations, and gather specific data points.
9
+
10
+ Strategy:
11
+ 1. Use describe_model to understand the model's columns and associations before querying
12
+ 2. Use execute_code to run ActiveRecord queries
13
+ 3. Follow associations to find related data (e.g., user -> booking_pages -> booking_requests)
14
+ 4. Use model methods to get computed values — NEVER fabricate URLs, tokens, or derived data manually
15
+
16
+ Rules:
17
+ - Always use describe_model before querying a model you haven't seen yet.
18
+ - Prefer ActiveRecord over raw SQL.
19
+ - When you find a model has methods that return what you need (check with
20
+ `.methods.grep(/pattern/)`), USE those methods instead of constructing values yourself.
21
+ - Include specific IDs, values, and counts in your findings.
22
+
23
+ End with a concise, factual summary of what you found. Include specific record IDs, values,
24
+ and relationships. Keep your summary under 300 words.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: Investigate code
3
+ description: Search and read source code to understand how a feature, pattern, or method works
4
+ max_rounds: 20
5
+ ---
6
+
7
+ You are a code investigator for a Rails application. Your job is to search the codebase,
8
+ read relevant files, and understand how a specific feature or pattern works.
9
+
10
+ Strategy:
11
+ 1. Start with search_code to find where the relevant code lives
12
+ 2. Use read_file to examine the key files (read specific sections, not entire files)
13
+ 3. Follow the chain: find the method, check what it calls, trace the data flow
14
+ 4. Use describe_model if database models are involved
15
+
16
+ Rules:
17
+ - Be thorough but efficient. Search first, then read specific files.
18
+ - If a search returns too many results, narrow it with a more specific query or directory.
19
+ - Look at actual method implementations, not just where things are called.
20
+ - Note file paths and line numbers in your findings.
21
+
22
+ End with a concise summary of your findings: key files, classes, methods, and how they
23
+ connect. Keep your summary under 500 words.
@@ -207,6 +207,7 @@ module RailsConsoleAi
207
207
  break if input.nil?
208
208
 
209
209
  input = input.strip
210
+ input = input.force_encoding('UTF-8') if input.encoding == Encoding::ASCII_8BIT
210
211
  break if input.downcase == 'exit' || input.downcase == 'quit'
211
212
  next if input.empty?
212
213
 
@@ -0,0 +1,84 @@
1
+ require 'rails_console_ai/channel/base'
2
+
3
+ module RailsConsoleAi
4
+ module Channel
5
+ class SubAgent < Base
6
+ def initialize(parent_channel:, task_label: nil)
7
+ @parent = parent_channel
8
+ @label = task_label
9
+ end
10
+
11
+ def display(text)
12
+ # Swallowed — sub-agent final text is returned as tool result, not displayed
13
+ end
14
+
15
+ def display_thinking(text)
16
+ @parent.display_thinking(text)
17
+ end
18
+
19
+ def display_status(text)
20
+ @parent.display_status(text)
21
+ end
22
+
23
+ def display_warning(text)
24
+ @parent.display_warning(text)
25
+ end
26
+
27
+ def display_error(text)
28
+ @parent.display_error(text)
29
+ end
30
+
31
+ def display_tool_call(text)
32
+ @parent.display_tool_call(text)
33
+ end
34
+
35
+ def display_code(code)
36
+ # Swallowed — sub-agent auto-executes, no need to show code
37
+ end
38
+
39
+ def display_result_output(output)
40
+ @parent.display_result_output(output)
41
+ end
42
+
43
+ def display_result(result)
44
+ # Swallowed — sub-agent return values aren't useful to show
45
+ end
46
+
47
+ def prompt(text)
48
+ '(sub-agent cannot ask user)'
49
+ end
50
+
51
+ def confirm(text)
52
+ 'y'
53
+ end
54
+
55
+ def user_identity
56
+ @parent.user_identity
57
+ end
58
+
59
+ def mode
60
+ 'sub_agent'
61
+ end
62
+
63
+ def cancelled?
64
+ @parent.cancelled?
65
+ end
66
+
67
+ def supports_danger?
68
+ false # Sub-agents must never silently bypass safety guards
69
+ end
70
+
71
+ def supports_editing?
72
+ false
73
+ end
74
+
75
+ def wrap_llm_call(&block)
76
+ yield
77
+ end
78
+
79
+ def system_instructions
80
+ nil
81
+ end
82
+ end
83
+ end
84
+ end
@@ -31,7 +31,9 @@ module RailsConsoleAi
31
31
  :code_search_paths,
32
32
  :channels,
33
33
  :bypass_guards_for_methods,
34
- :user_extra_info
34
+ :user_extra_info,
35
+ :sub_agent_max_rounds,
36
+ :sub_agent_model
35
37
 
36
38
  def initialize
37
39
  @provider = :anthropic
@@ -64,6 +66,8 @@ module RailsConsoleAi
64
66
  @channels = {}
65
67
  @bypass_guards_for_methods = []
66
68
  @user_extra_info = {}
69
+ @sub_agent_max_rounds = 15
70
+ @sub_agent_model = nil
67
71
  end
68
72
 
69
73
  def resolve_user_extra_info(username)
@@ -20,6 +20,7 @@ module RailsConsoleAi
20
20
  parts << guide_context
21
21
  parts << trusted_methods_context
22
22
  parts << skills_context
23
+ parts << agents_context
23
24
  parts << user_extra_info_context
24
25
  parts << pinned_memory_context
25
26
  parts << memory_context
@@ -93,6 +94,14 @@ module RailsConsoleAi
93
94
  a new skill with a step-by-step recipe. Skills are procedures (how to do X); memories
94
95
  are facts (what you learned about X). Do NOT use save_memory when asked to create a skill.
95
96
 
97
+ You have a delegate_task tool to spawn sub-agents for investigation tasks:
98
+ - Use delegate_task when a task requires multiple tool calls to investigate
99
+ (e.g., finding a user's shard, exploring how a feature works in the code,
100
+ gathering data across multiple models).
101
+ - The sub-agent runs in a separate context and returns only a concise summary.
102
+ - This keeps your conversation context small and efficient.
103
+ - If a custom agent is available for the task (see Agents section), specify it by name.
104
+
96
105
  RULES:
97
106
  - Give ONE concise answer. Do not offer multiple alternatives or variations.
98
107
  - For multi-step tasks, use execute_plan to break the work into small, clear steps.
@@ -145,6 +154,19 @@ module RailsConsoleAi
145
154
  nil
146
155
  end
147
156
 
157
+ def agents_context
158
+ require 'rails_console_ai/agent_loader'
159
+ summaries = RailsConsoleAi::AgentLoader.new.agent_summaries
160
+ return nil if summaries.nil? || summaries.empty?
161
+
162
+ lines = ["## Agents (use delegate_task tool to invoke)"]
163
+ lines.concat(summaries)
164
+ lines.join("\n")
165
+ rescue => e
166
+ RailsConsoleAi.logger.debug("RailsConsoleAi: agents context failed: #{e.message}")
167
+ nil
168
+ end
169
+
148
170
  def user_extra_info_context
149
171
  info = @config.resolve_user_extra_info(@user_name)
150
172
  return nil if info.nil? || info.strip.empty?
@@ -249,7 +249,7 @@ module RailsConsoleAi
249
249
  output_id = @executor.store_output(result_str)
250
250
  if result_str.length > LARGE_OUTPUT_THRESHOLD
251
251
  preview = result_str[0, LARGE_OUTPUT_PREVIEW_CHARS]
252
- context_msg += "\n#{preview}\n\n[Output truncated at #{LARGE_OUTPUT_PREVIEW_CHARS} of #{result_str.length} chars — use recall_output tool with id #{output_id} to retrieve the full output]"
252
+ context_msg += "\n#{preview}\n\n[Output truncated at #{LARGE_OUTPUT_PREVIEW_CHARS} of #{result_str.length} chars — use explore_output with output_id=#{output_id} for focused queries, or recall_output to expand in place]"
253
253
  elsif !output_parts.empty?
254
254
  context_msg += "\n#{result_str}"
255
255
  end
@@ -332,7 +332,7 @@ module RailsConsoleAi
332
332
  context_msg = "Code was executed (safety override). "
333
333
  if result_str.length > LARGE_OUTPUT_THRESHOLD
334
334
  context_msg += result_str[0, LARGE_OUTPUT_PREVIEW_CHARS]
335
- context_msg += "\n\n[Output truncated at #{LARGE_OUTPUT_PREVIEW_CHARS} of #{result_str.length} chars — use recall_output tool with id #{output_id} to retrieve the full output]"
335
+ context_msg += "\n\n[Output truncated at #{LARGE_OUTPUT_PREVIEW_CHARS} of #{result_str.length} chars — use explore_output with output_id=#{output_id} for focused queries, or recall_output to expand in place]"
336
336
  else
337
337
  context_msg += result_str
338
338
  end
@@ -360,7 +360,7 @@ module RailsConsoleAi
360
360
  context_msg = "Code was executed. "
361
361
  if result_str.length > LARGE_OUTPUT_THRESHOLD
362
362
  context_msg += result_str[0, LARGE_OUTPUT_PREVIEW_CHARS]
363
- context_msg += "\n\n[Output truncated at #{LARGE_OUTPUT_PREVIEW_CHARS} of #{result_str.length} chars — use recall_output tool with id #{output_id} to retrieve the full output]"
363
+ context_msg += "\n\n[Output truncated at #{LARGE_OUTPUT_PREVIEW_CHARS} of #{result_str.length} chars — use explore_output with output_id=#{output_id} for focused queries, or recall_output to expand in place]"
364
364
  else
365
365
  context_msg += result_str
366
366
  end
@@ -882,6 +882,17 @@ module RailsConsoleAi
882
882
  @channel.display_status(" #{preview}#{cached_tag}")
883
883
  end
884
884
 
885
+ # Aggregate sub-agent token usage into parent's cost tracking
886
+ if tc[:name] == 'delegate_task' && tools.last_sub_agent_usage
887
+ sa = tools.last_sub_agent_usage
888
+ @total_input_tokens += sa[:input] || 0
889
+ @total_output_tokens += sa[:output] || 0
890
+ if sa[:model]
891
+ @token_usage[sa[:model]][:input] += sa[:input] || 0
892
+ @token_usage[sa[:model]][:output] += sa[:output] || 0
893
+ end
894
+ end
895
+
885
896
  if RailsConsoleAi.configuration.debug
886
897
  $stderr.puts "\e[35m[debug] tool result (#{tool_result.to_s.length} chars)\e[0m"
887
898
  end
@@ -892,7 +903,7 @@ module RailsConsoleAi
892
903
  tool_msg[:output_id] = output_id
893
904
  if full_text.length > LARGE_OUTPUT_THRESHOLD
894
905
  truncated = full_text[0, LARGE_OUTPUT_PREVIEW_CHARS]
895
- truncated += "\n\n[Output truncated at #{LARGE_OUTPUT_PREVIEW_CHARS} of #{full_text.length} chars — use recall_output tool with id #{output_id} to retrieve the full output]"
906
+ truncated += "\n\n[Output truncated at #{LARGE_OUTPUT_PREVIEW_CHARS} of #{full_text.length} chars — use explore_output with output_id=#{output_id} for focused queries, or recall_output to expand in place]"
896
907
  tool_msg = provider.format_tool_result(tc[:id], truncated)
897
908
  tool_msg[:output_id] = output_id
898
909
  end
@@ -1030,9 +1041,18 @@ module RailsConsoleAi
1030
1041
  when 'save_skill' then "(\"#{args['name']}\")"
1031
1042
  when 'delete_skill' then "(\"#{args['name']}\")"
1032
1043
  when 'recall_output' then "(#{args['id']})"
1044
+ when 'explore_output'
1045
+ task_preview = args['task'].to_s[0, 80]
1046
+ task_preview += '...' if args['task'].to_s.length > 80
1047
+ "(id: #{args['output_id']}, \"#{task_preview}\")"
1033
1048
  when 'execute_plan'
1034
1049
  steps = args['steps']
1035
1050
  steps ? "(#{steps.length} steps)" : ''
1051
+ when 'delegate_task'
1052
+ task_preview = args['task'].to_s[0, 100]
1053
+ task_preview += '...' if args['task'].to_s.length > 100
1054
+ agent = args['agent'] ? ", agent: \"#{args['agent']}\"" : ''
1055
+ "(\"#{task_preview}\"#{agent})"
1036
1056
  else ''
1037
1057
  end
1038
1058
  end
@@ -1104,6 +1124,9 @@ module RailsConsoleAi
1104
1124
  when 'execute_plan'
1105
1125
  steps_done = result.scan(/^Step \d+/).length
1106
1126
  steps_done > 0 ? "#{steps_done} steps executed" : truncate(result, 80)
1127
+ when 'delegate_task'
1128
+ # Show the full sub-agent result — this is the whole point of delegation
1129
+ result
1107
1130
  else
1108
1131
  truncate(result, 80)
1109
1132
  end
@@ -1390,7 +1413,7 @@ module RailsConsoleAi
1390
1413
  end
1391
1414
 
1392
1415
  def trim_message(msg)
1393
- ref = "[Output omitted — use recall_output tool with id #{msg[:output_id]} to retrieve]"
1416
+ ref = "[Output omitted — use explore_output with output_id=#{msg[:output_id]} for focused queries, or recall_output to expand in place]"
1394
1417
 
1395
1418
  if msg[:content].is_a?(Array)
1396
1419
  trimmed_content = msg[:content].map do |block|
@@ -0,0 +1,229 @@
1
+ require 'rails_console_ai/channel/sub_agent'
2
+ require 'rails_console_ai/tools/registry'
3
+ require 'rails_console_ai/providers/base'
4
+ require 'rails_console_ai/executor'
5
+
6
+ module RailsConsoleAi
7
+ class SubAgent
8
+ LOOP_WARN_THRESHOLD = 3
9
+ LOOP_BREAK_THRESHOLD = 5
10
+ LARGE_OUTPUT_THRESHOLD = 10_000
11
+ LARGE_OUTPUT_PREVIEW_CHARS = 8_000
12
+
13
+ attr_reader :input_tokens, :output_tokens, :model_used
14
+
15
+ def initialize(task:, agent_config:, binding_context:, parent_channel:, executor:,
16
+ output_payload: nil, output_local_name: :output)
17
+ @task = task
18
+ @agent_config = agent_config || {}
19
+ @binding_context = binding_context
20
+ @parent_channel = parent_channel
21
+ @parent_executor = executor
22
+ @output_payload = output_payload
23
+ @output_local_name = output_local_name
24
+ @input_tokens = 0
25
+ @output_tokens = 0
26
+ @model_used = nil
27
+ end
28
+
29
+ def run
30
+ channel = Channel::SubAgent.new(
31
+ parent_channel: @parent_channel,
32
+ task_label: @agent_config['name']
33
+ )
34
+
35
+ effective_binding =
36
+ if @output_payload
37
+ b = @binding_context.eval("proc { binding }.call")
38
+ b.local_variable_set(@output_local_name, @output_payload)
39
+ b
40
+ else
41
+ @binding_context
42
+ end
43
+
44
+ executor = Executor.new(effective_binding, channel: channel)
45
+ allowed_tools = @agent_config['tools'] ? Array(@agent_config['tools']) : nil
46
+ tools = Tools::Registry.new(executor: executor, mode: :sub_agent, channel: channel, allowed_tools: allowed_tools)
47
+ provider = build_provider
48
+ system_prompt = build_system_prompt
49
+ max_rounds = @agent_config['max_rounds'] || RailsConsoleAi.configuration.sub_agent_max_rounds
50
+
51
+ messages = [{ role: :user, content: @task }]
52
+
53
+ run_tool_loop(messages, system_prompt: system_prompt, tools: tools,
54
+ provider: provider, channel: channel, executor: executor,
55
+ max_rounds: max_rounds)
56
+ end
57
+
58
+ private
59
+
60
+ def run_tool_loop(messages, system_prompt:, tools:, provider:, channel:, executor:, max_rounds:)
61
+ result = nil
62
+ tool_call_counts = Hash.new(0)
63
+ exhausted = false
64
+ last_thinking = nil
65
+
66
+ max_rounds.times do |round|
67
+ break if channel.cancelled?
68
+
69
+ if round == 0
70
+ channel.display_status("Thinking...")
71
+ end
72
+
73
+ begin
74
+ result = provider.chat_with_tools(messages, tools: tools, system_prompt: system_prompt)
75
+ rescue Providers::ProviderError => e
76
+ raise
77
+ end
78
+ @input_tokens += result.input_tokens || 0
79
+ @output_tokens += result.output_tokens || 0
80
+
81
+ break if channel.cancelled?
82
+ break unless result.tool_use?
83
+
84
+ # Display the LLM's reasoning text before executing its tool calls
85
+ if result.text && !result.text.strip.empty?
86
+ result.text.strip.split("\n").each do |line|
87
+ channel.display_thinking(" #{line}")
88
+ end
89
+ end
90
+
91
+ assistant_msg = provider.format_assistant_message(result)
92
+ messages << assistant_msg
93
+
94
+ result.tool_calls.each do |tc|
95
+ break if channel.cancelled?
96
+
97
+ args_display = tc[:arguments].map { |k, v|
98
+ val = v.to_s
99
+ val = val[0, 60] + '...' if val.length > 60
100
+ "#{k}: #{val.inspect}"
101
+ }.join(', ')
102
+ channel.display_tool_call("#{tc[:name]}(#{args_display})")
103
+
104
+ tool_result = tools.execute(tc[:name], tc[:arguments])
105
+
106
+ preview = tool_result.to_s.lines.first(3).join.strip
107
+ preview = preview[0, 120] + '...' if preview.length > 120
108
+ channel.display_status(" #{preview}")
109
+
110
+ # Truncate large outputs to keep sub-agent context lean
111
+ tool_result_str = tool_result.to_s
112
+ if tool_result_str.length > LARGE_OUTPUT_THRESHOLD
113
+ tool_result_str = tool_result_str[0, LARGE_OUTPUT_PREVIEW_CHARS] +
114
+ "\n\n[Output truncated at #{LARGE_OUTPUT_PREVIEW_CHARS} of #{tool_result_str.length} chars]"
115
+ end
116
+
117
+ tool_msg = provider.format_tool_result(tc[:id], tool_result_str)
118
+ messages << tool_msg
119
+ end
120
+
121
+ # Loop detection
122
+ result.tool_calls.each do |tc|
123
+ key = "#{tc[:name]}:#{tc[:arguments].to_json}"
124
+ tool_call_counts[key] += 1
125
+
126
+ if tool_call_counts[key] >= LOOP_BREAK_THRESHOLD
127
+ channel.display_status("Loop detected — stopping.")
128
+ exhausted = true
129
+ elsif tool_call_counts[key] >= LOOP_WARN_THRESHOLD
130
+ messages << { role: :user, content: "You are repeating the same tool call with the same arguments. Try a different approach or provide your answer now." }
131
+ end
132
+ end
133
+ break if exhausted
134
+
135
+ break if executor.last_cancelled?
136
+
137
+ exhausted = true if round == max_rounds - 1
138
+ end
139
+
140
+ if exhausted
141
+ messages << { role: :user, content: "Provide your best answer now based on what you've learned." }
142
+ result = provider.chat(messages, system_prompt: system_prompt)
143
+ @input_tokens += result.input_tokens || 0
144
+ @output_tokens += result.output_tokens || 0
145
+ end
146
+
147
+ result&.text || '(sub-agent returned no result)'
148
+ end
149
+
150
+ def build_provider
151
+ config = RailsConsoleAi.configuration
152
+ model_override = @agent_config['model'] || config.sub_agent_model
153
+
154
+ if model_override
155
+ config_dup = config.dup
156
+ config_dup.model = model_override
157
+ @model_used = model_override
158
+ Providers.build(config_dup)
159
+ else
160
+ @model_used = config.resolved_model
161
+ Providers.build(config)
162
+ end
163
+ end
164
+
165
+ def build_system_prompt
166
+ parts = []
167
+ parts << base_instructions
168
+ parts << guide_context
169
+ parts << pinned_memory_context
170
+ parts << @agent_config['body'] if @agent_config['body'] && !@agent_config['body'].strip.empty?
171
+ parts.compact.join("\n\n")
172
+ end
173
+
174
+ def base_instructions
175
+ <<~PROMPT.strip
176
+ You are a sub-agent assistant for a Ruby on Rails application. You have been delegated
177
+ a specific investigation task. Your job is to use the available tools to find the answer
178
+ efficiently, then provide a concise summary of your findings.
179
+
180
+ RULES:
181
+ - Focus on the specific task you were given. Do not go on tangents.
182
+ - Use tools to look up schema/model details rather than guessing.
183
+ - Prefer ActiveRecord query interface over raw SQL.
184
+ - Use describe_model to understand models before querying them.
185
+ - NEVER fabricate URLs, IDs, or data. Always look things up using model methods.
186
+ - When you find methods on a model (e.g. via .methods.grep), USE them rather than
187
+ constructing values manually.
188
+ - End with a concise, factual summary. Include specific IDs, values, and findings.
189
+ - Keep your final answer under 300 words.
190
+ PROMPT
191
+ end
192
+
193
+ def guide_context
194
+ content = RailsConsoleAi.storage.read(RailsConsoleAi::GUIDE_KEY)
195
+ return nil if content.nil? || content.strip.empty?
196
+
197
+ "## Application Guide\n\n#{content.strip}"
198
+ rescue => e
199
+ RailsConsoleAi.logger.debug("SubAgent: guide context failed: #{e.message}")
200
+ nil
201
+ end
202
+
203
+ def pinned_memory_context
204
+ channel_mode = @parent_channel.respond_to?(:mode) ? @parent_channel.mode : nil
205
+ # For sub-agents spawned from Slack, use the Slack channel's pinned memories.
206
+ # For sub-agents spawned from console, use the console channel's pinned memories.
207
+ # Fall back to 'slack' if the parent is a sub_agent channel (nested, though we block this).
208
+ effective_mode = channel_mode == 'sub_agent' ? 'slack' : channel_mode
209
+ return nil unless effective_mode
210
+
211
+ channel_cfg = RailsConsoleAi.configuration.channels[effective_mode] || {}
212
+ pinned_tags = channel_cfg['pinned_memory_tags'] || []
213
+ return nil if pinned_tags.empty?
214
+
215
+ require 'rails_console_ai/tools/memory_tools'
216
+ sections = pinned_tags.filter_map do |tag|
217
+ content = Tools::MemoryTools.new.recall_memories(tag: tag)
218
+ next if content.nil? || content.include?("No memories")
219
+ content
220
+ end
221
+ return nil if sections.empty?
222
+
223
+ "## Key Context (from pinned memories)\n\n" + sections.join("\n\n")
224
+ rescue => e
225
+ RailsConsoleAi.logger.debug("SubAgent: pinned memory context failed: #{e.message}")
226
+ nil
227
+ end
228
+ end
229
+ end
@@ -37,8 +37,8 @@ module RailsConsoleAi
37
37
 
38
38
  # Columns and indexes from the database table
39
39
  begin
40
- if ActiveRecord::Base.connected?
41
- conn = ActiveRecord::Base.connection
40
+ conn = model.connection
41
+ if conn
42
42
  if conn.tables.include?(model.table_name)
43
43
  cols = conn.columns(model.table_name).map do |c|
44
44
  parts = ["#{c.name}:#{c.type}"]
@@ -3,19 +3,21 @@ require 'json'
3
3
  module RailsConsoleAi
4
4
  module Tools
5
5
  class Registry
6
- attr_reader :definitions
6
+ attr_reader :definitions, :last_sub_agent_usage
7
7
 
8
8
  # Tools that should never be cached (side effects or user interaction)
9
- NO_CACHE = %w[ask_user save_memory delete_memory recall_memory execute_code execute_plan activate_skill save_skill delete_skill].freeze
9
+ NO_CACHE = %w[ask_user save_memory delete_memory recall_memory execute_code execute_plan activate_skill save_skill delete_skill delegate_task explore_output].freeze
10
10
 
11
- def initialize(executor: nil, mode: :default, channel: nil)
11
+ def initialize(executor: nil, mode: :default, channel: nil, allowed_tools: nil)
12
12
  @executor = executor
13
13
  @mode = mode
14
14
  @channel = channel
15
+ @allowed_tools = allowed_tools
15
16
  @definitions = []
16
17
  @handlers = {}
17
18
  @cache = {}
18
19
  @last_cached = false
20
+ @last_sub_agent_usage = nil
19
21
  register_all
20
22
  end
21
23
 
@@ -186,7 +188,7 @@ module RailsConsoleAi
186
188
  if @executor
187
189
  register(
188
190
  name: 'recall_output',
189
- description: 'Retrieve a previous code execution output that was omitted or truncated. The output will be expanded in place in the conversation. Use the output id shown in the "[Output omitted]" or "[Output truncated]" placeholder.',
191
+ description: 'Expand a previously omitted/truncated output back into this conversation\'s context, where it will persist for the rest of the session. Prefer `explore_output` if you only need a specific answer about the output — that keeps this conversation lean. Use `recall_output` only when you need the full content alongside other context here. Use the output id shown in the "[Output omitted]" or "[Output truncated]" placeholder.',
190
192
  parameters: {
191
193
  'type' => 'object',
192
194
  'properties' => {
@@ -202,7 +204,7 @@ module RailsConsoleAi
202
204
 
203
205
  register(
204
206
  name: 'recall_outputs',
205
- description: 'Retrieve multiple previous code execution outputs that were omitted from the conversation. Use the output ids shown in "[Output omitted]" or "[Output truncated]" placeholders.',
207
+ description: 'Expand multiple previously omitted outputs back into this conversation. Prefer `explore_output` per-id for focused queries. Use the output ids shown in "[Output omitted]" or "[Output truncated]" placeholders.',
206
208
  parameters: {
207
209
  'type' => 'object',
208
210
  'properties' => {
@@ -212,26 +214,188 @@ module RailsConsoleAi
212
214
  },
213
215
  handler: ->(args) { "recall_outputs handled by conversation engine" }
214
216
  )
217
+
218
+ if @mode != :sub_agent
219
+ register(
220
+ name: 'explore_output',
221
+ description: 'Prefer this over recall_output when you have a specific question about a large omitted/truncated output (e.g. "find the item where X", "how many match Y", "what is the value at index N", "parse the JSON and return field Z"). Spawns a sub-agent with the full output bound to the local Ruby variable `output` (a String); the sub-agent runs execute_code against it and returns a concise answer. The full output does NOT enter this conversation.',
222
+ parameters: {
223
+ 'type' => 'object',
224
+ 'properties' => {
225
+ 'output_id' => { 'type' => 'integer', 'description' => 'The output id shown in the "[Output omitted]" or "[Output truncated]" placeholder.' },
226
+ 'task' => { 'type' => 'string', 'description' => 'The specific question or task. Be concrete — the sub-agent only sees this task and the output.' }
227
+ },
228
+ 'required' => ['output_id', 'task']
229
+ },
230
+ handler: ->(args) { explore_output(args['output_id'].to_i, args['task']) }
231
+ )
232
+ end
215
233
  end
216
234
 
217
235
  unless @mode == :init
218
- register(
219
- name: 'ask_user',
220
- 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.',
221
- parameters: {
222
- 'type' => 'object',
223
- 'properties' => {
224
- 'question' => { 'type' => 'string', 'description' => 'The question to ask the user' }
236
+ # Sub-agents get execute_code and read-only memory tools, but not ask_user,
237
+ # memory writes, skill writes, execute_plan, or delegate_task.
238
+ if @mode == :sub_agent
239
+ register_memory_recall_tools
240
+ register_execute_code
241
+ else
242
+ register(
243
+ name: 'ask_user',
244
+ 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.',
245
+ parameters: {
246
+ 'type' => 'object',
247
+ 'properties' => {
248
+ 'question' => { 'type' => 'string', 'description' => 'The question to ask the user' }
249
+ },
250
+ 'required' => ['question']
225
251
  },
226
- 'required' => ['question']
252
+ handler: ->(args) { ask_user(args['question']) }
253
+ )
254
+
255
+ register_memory_tools
256
+ register_skill_tools
257
+ register_execute_plan
258
+ register_delegate_task
259
+ end
260
+ end
261
+ end
262
+
263
+ def register_memory_recall_tools
264
+ return unless RailsConsoleAi.configuration.memories_enabled
265
+
266
+ require 'rails_console_ai/tools/memory_tools'
267
+ memory = MemoryTools.new
268
+
269
+ register(
270
+ name: 'recall_memory',
271
+ description: 'Retrieve a specific memory by name.',
272
+ parameters: {
273
+ 'type' => 'object',
274
+ 'properties' => {
275
+ 'name' => { 'type' => 'string', 'description' => 'The exact memory name' }
227
276
  },
228
- handler: ->(args) { ask_user(args['question']) }
229
- )
277
+ 'required' => ['name']
278
+ },
279
+ handler: ->(args) { memory.recall_memory(name: args['name']) }
280
+ )
281
+
282
+ register(
283
+ name: 'recall_memories',
284
+ description: 'Search saved memories about this codebase. Call with no args to list all, or pass a query/tag to filter.',
285
+ parameters: {
286
+ 'type' => 'object',
287
+ 'properties' => {
288
+ 'query' => { 'type' => 'string', 'description' => 'Search term to filter by name, description, or tags' },
289
+ 'tag' => { 'type' => 'string', 'description' => 'Filter by a specific tag' }
290
+ }
291
+ },
292
+ handler: ->(args) { memory.recall_memories(query: args['query'], tag: args['tag']) }
293
+ )
294
+ end
295
+
296
+ def register_execute_code
297
+ return unless @executor
298
+
299
+ register(
300
+ name: 'execute_code',
301
+ description: 'Execute Ruby code in the Rails console and return the result.',
302
+ parameters: {
303
+ 'type' => 'object',
304
+ 'properties' => {
305
+ 'code' => { 'type' => 'string', 'description' => 'Ruby code to execute' }
306
+ },
307
+ 'required' => ['code']
308
+ },
309
+ handler: ->(args) { execute_code(args['code']) }
310
+ )
311
+ end
312
+
313
+ def register_delegate_task
314
+ return unless @executor
230
315
 
231
- register_memory_tools
232
- register_skill_tools
233
- register_execute_plan
316
+ register(
317
+ name: 'delegate_task',
318
+ description: 'Delegate an investigation to a sub-agent that runs in a separate context. ' \
319
+ 'Use this for tasks that require multiple tool calls to figure out ' \
320
+ '(e.g., "find which shard user X is on", "search the codebase for how URL generation works", ' \
321
+ '"describe the relationships between models A, B, and C"). ' \
322
+ 'The sub-agent runs independently and returns only a concise summary, ' \
323
+ 'keeping this conversation\'s context small and efficient.',
324
+ parameters: {
325
+ 'type' => 'object',
326
+ 'properties' => {
327
+ 'task' => { 'type' => 'string', 'description' => 'What to investigate. Be specific about what you need to know.' },
328
+ 'agent' => { 'type' => 'string', 'description' => 'Optional: name of a custom agent to use (see Agents list in system prompt). Omit for general investigation.' }
329
+ },
330
+ 'required' => ['task']
331
+ },
332
+ handler: ->(args) { delegate_task(args['task'], args['agent']) }
333
+ )
334
+ end
335
+
336
+ EXPLORE_OUTPUT_AGENT_CONFIG = {
337
+ 'name' => 'output-explorer',
338
+ 'tools' => ['execute_code'],
339
+ 'max_rounds' => 8,
340
+ 'body' => <<~PROMPT.freeze
341
+ You are exploring a single chunk of captured tool output on behalf of the main assistant.
342
+
343
+ The full output is bound to the local variable `output` (a String). You do NOT see it
344
+ directly — it lives in Ruby memory. Use `execute_code` with Ruby to query it:
345
+ - `output.length`, `output.lines.count`
346
+ - `output[start, len]`, `output.lines[n]`
347
+ - `output.scan(/pattern/)`, `output.include?("...")`
348
+ - `JSON.parse(output)` if it looks like JSON, then drill in
349
+ - any other Ruby string/collection methods
350
+
351
+ Print only the specific slice or summary the task requires — never dump the whole `output`.
352
+ Return a concise factual answer. No preamble.
353
+ PROMPT
354
+ }.freeze
355
+
356
+ def explore_output(output_id, task)
357
+ require 'rails_console_ai/sub_agent'
358
+
359
+ payload = @executor.recall_output(output_id)
360
+ return "No output found with id #{output_id}" unless payload
361
+
362
+ sub = SubAgent.new(
363
+ task: task,
364
+ agent_config: EXPLORE_OUTPUT_AGENT_CONFIG,
365
+ binding_context: @executor.binding_context,
366
+ parent_channel: @channel,
367
+ executor: @executor,
368
+ output_payload: payload.dup
369
+ )
370
+ result = sub.run
371
+ @last_sub_agent_usage = { input: sub.input_tokens, output: sub.output_tokens, model: sub.model_used }
372
+ "Exploration result (#{sub.input_tokens + sub.output_tokens} tokens used, #{payload.length} chars explored):\n#{result}"
373
+ end
374
+
375
+ def delegate_task(task, agent_name = nil)
376
+ require 'rails_console_ai/sub_agent'
377
+ require 'rails_console_ai/agent_loader'
378
+
379
+ agent_config = nil
380
+ if agent_name
381
+ loader = AgentLoader.new
382
+ agent_config = loader.find_agent(agent_name)
383
+ unless agent_config
384
+ available = loader.load_all_agents.map { |a| a['name'] }
385
+ return "Agent not found: \"#{agent_name}\". Available agents: #{available.join(', ')}"
386
+ end
234
387
  end
388
+
389
+ sub = SubAgent.new(
390
+ task: task,
391
+ agent_config: agent_config,
392
+ binding_context: @executor.binding_context,
393
+ parent_channel: @channel,
394
+ executor: @executor
395
+ )
396
+ result = sub.run
397
+ @last_sub_agent_usage = { input: sub.input_tokens, output: sub.output_tokens, model: sub.model_used }
398
+ "Sub-agent result (#{sub.input_tokens + sub.output_tokens} tokens used):\n#{result}"
235
399
  end
236
400
 
237
401
  def register_memory_tools
@@ -412,7 +576,7 @@ module RailsConsoleAi
412
576
  # Show the code to the user
413
577
  @executor.display_code_block(code)
414
578
 
415
- exec_result = if @channel&.mode == 'slack' || RailsConsoleAi.configuration.auto_execute
579
+ exec_result = if @channel&.mode == 'slack' || @channel&.mode == 'sub_agent' || RailsConsoleAi.configuration.auto_execute
416
580
  @executor.execute(code)
417
581
  else
418
582
  @executor.confirm_and_execute(code)
@@ -547,6 +711,13 @@ module RailsConsoleAi
547
711
  end
548
712
  step_report += "Return value: #{exec_result.inspect}"
549
713
  results << step_report
714
+
715
+ # Stop on error so the LLM can fix the failed step before continuing
716
+ if error
717
+ remaining = steps.length - i - 1
718
+ results << "PLAN HALTED: Step #{i + 1} failed. #{remaining} remaining step(s) were not executed. Fix the error and retry the remaining steps." if remaining > 0
719
+ break
720
+ end
550
721
  end
551
722
 
552
723
  results.join("\n\n")
@@ -609,6 +780,11 @@ module RailsConsoleAi
609
780
  end
610
781
 
611
782
  def register(name:, description:, parameters:, handler:)
783
+ # When allowed_tools is set (sub-agent with tool filter), skip tools not in the list
784
+ if @allowed_tools && !@allowed_tools.include?(name)
785
+ return
786
+ end
787
+
612
788
  @definitions << {
613
789
  name: name,
614
790
  description: description,
@@ -18,18 +18,19 @@ module RailsConsoleAi
18
18
  return "Error: table_name is required." if table_name.nil? || table_name.strip.empty?
19
19
 
20
20
  table_name = table_name.strip
21
- unless connection.tables.include?(table_name)
21
+ conn = connection_for_table(table_name)
22
+ unless conn.tables.include?(table_name)
22
23
  return "Table '#{table_name}' not found. Use list_tables to see available tables."
23
24
  end
24
25
 
25
- cols = connection.columns(table_name).map do |c|
26
+ cols = conn.columns(table_name).map do |c|
26
27
  parts = ["#{c.name}:#{c.type}"]
27
28
  parts << "nullable" if c.null
28
29
  parts << "default=#{c.default}" unless c.default.nil?
29
30
  parts.join(" ")
30
31
  end
31
32
 
32
- indexes = connection.indexes(table_name).map do |idx|
33
+ indexes = conn.indexes(table_name).map do |idx|
33
34
  unique = idx.unique ? "UNIQUE " : ""
34
35
  "#{unique}INDEX on (#{idx.columns.join(', ')})"
35
36
  end
@@ -52,6 +53,22 @@ module RailsConsoleAi
52
53
  defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
53
54
  end
54
55
 
56
+ # Find the best connection for a table by checking if any model maps to it.
57
+ # Models may use a different connection (e.g. sharded databases).
58
+ def connection_for_table(table_name)
59
+ if defined?(ActiveRecord::Base) && ActiveRecord::Base.is_a?(Class)
60
+ base = defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base
61
+ model = ObjectSpace.each_object(Class).detect { |c|
62
+ c < base && !c.abstract_class? && c.name &&
63
+ begin; c.table_name == table_name; rescue; false; end
64
+ }
65
+ return model.connection if model
66
+ end
67
+ connection
68
+ rescue
69
+ connection
70
+ end
71
+
55
72
  def connection
56
73
  ActiveRecord::Base.connection
57
74
  end
@@ -1,3 +1,3 @@
1
1
  module RailsConsoleAi
2
- VERSION = '0.25.0'.freeze
2
+ VERSION = '0.27.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_console_ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.25.0
4
+ version: 0.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cortfr
@@ -100,9 +100,13 @@ files:
100
100
  - lib/generators/rails_console_ai/install_generator.rb
101
101
  - lib/generators/rails_console_ai/templates/initializer.rb
102
102
  - lib/rails_console_ai.rb
103
+ - lib/rails_console_ai/agent_loader.rb
104
+ - lib/rails_console_ai/agents/explore-data.md
105
+ - lib/rails_console_ai/agents/investigate-code.md
103
106
  - lib/rails_console_ai/channel/base.rb
104
107
  - lib/rails_console_ai/channel/console.rb
105
108
  - lib/rails_console_ai/channel/slack.rb
109
+ - lib/rails_console_ai/channel/sub_agent.rb
106
110
  - lib/rails_console_ai/configuration.rb
107
111
  - lib/rails_console_ai/console_methods.rb
108
112
  - lib/rails_console_ai/context_builder.rb
@@ -123,6 +127,7 @@ files:
123
127
  - lib/rails_console_ai/slack_bot.rb
124
128
  - lib/rails_console_ai/storage/base.rb
125
129
  - lib/rails_console_ai/storage/file_storage.rb
130
+ - lib/rails_console_ai/sub_agent.rb
126
131
  - lib/rails_console_ai/tools/code_tools.rb
127
132
  - lib/rails_console_ai/tools/memory_tools.rb
128
133
  - lib/rails_console_ai/tools/model_tools.rb