rails_console_ai 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +61 -0
- data/lib/rails_console_ai/agent_loader.rb +126 -0
- data/lib/rails_console_ai/agents/explore-data.md +24 -0
- data/lib/rails_console_ai/agents/investigate-code.md +23 -0
- data/lib/rails_console_ai/channel/sub_agent.rb +84 -0
- data/lib/rails_console_ai/configuration.rb +5 -1
- data/lib/rails_console_ai/context_builder.rb +22 -0
- data/lib/rails_console_ai/conversation_engine.rb +19 -0
- data/lib/rails_console_ai/sub_agent.rb +217 -0
- data/lib/rails_console_ai/tools/model_tools.rb +2 -2
- data/lib/rails_console_ai/tools/registry.rb +138 -17
- data/lib/rails_console_ai/tools/schema_tools.rb +20 -3
- data/lib/rails_console_ai/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2194416a93ce8522de376169eb627b90c8b2477ba253f0f1b877144251f9ee6
|
|
4
|
+
data.tar.gz: 65dcbeb6eef9dd2641181529aa671058e85a6cd1c5263acd51678c52c89d3567
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4d5a1f2fe8ef6ae4593829ac1a8997eeeac652ea0d0a491f91a64b8e45a246d024a10aa269e853949e0d106779d4f1b9d671af21057bd6af802ff6a541ac15d
|
|
7
|
+
data.tar.gz: ec0e0d25f7bb2605a7c4180ebc56ab56d8f590d3632c98310ec65c3aecd987d04118e7199534a8ea5a1a8023a505de9ac467ce05d01393df667c2436bcebc68c
|
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.
|
|
@@ -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?
|
|
@@ -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
|
|
@@ -1033,6 +1044,11 @@ module RailsConsoleAi
|
|
|
1033
1044
|
when 'execute_plan'
|
|
1034
1045
|
steps = args['steps']
|
|
1035
1046
|
steps ? "(#{steps.length} steps)" : ''
|
|
1047
|
+
when 'delegate_task'
|
|
1048
|
+
task_preview = args['task'].to_s[0, 100]
|
|
1049
|
+
task_preview += '...' if args['task'].to_s.length > 100
|
|
1050
|
+
agent = args['agent'] ? ", agent: \"#{args['agent']}\"" : ''
|
|
1051
|
+
"(\"#{task_preview}\"#{agent})"
|
|
1036
1052
|
else ''
|
|
1037
1053
|
end
|
|
1038
1054
|
end
|
|
@@ -1104,6 +1120,9 @@ module RailsConsoleAi
|
|
|
1104
1120
|
when 'execute_plan'
|
|
1105
1121
|
steps_done = result.scan(/^Step \d+/).length
|
|
1106
1122
|
steps_done > 0 ? "#{steps_done} steps executed" : truncate(result, 80)
|
|
1123
|
+
when 'delegate_task'
|
|
1124
|
+
# Show the full sub-agent result — this is the whole point of delegation
|
|
1125
|
+
result
|
|
1107
1126
|
else
|
|
1108
1127
|
truncate(result, 80)
|
|
1109
1128
|
end
|
|
@@ -0,0 +1,217 @@
|
|
|
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
|
+
@task = task
|
|
17
|
+
@agent_config = agent_config || {}
|
|
18
|
+
@binding_context = binding_context
|
|
19
|
+
@parent_channel = parent_channel
|
|
20
|
+
@parent_executor = executor
|
|
21
|
+
@input_tokens = 0
|
|
22
|
+
@output_tokens = 0
|
|
23
|
+
@model_used = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run
|
|
27
|
+
channel = Channel::SubAgent.new(
|
|
28
|
+
parent_channel: @parent_channel,
|
|
29
|
+
task_label: @agent_config['name']
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
executor = Executor.new(@binding_context, channel: channel)
|
|
33
|
+
allowed_tools = @agent_config['tools'] ? Array(@agent_config['tools']) : nil
|
|
34
|
+
tools = Tools::Registry.new(executor: executor, mode: :sub_agent, channel: channel, allowed_tools: allowed_tools)
|
|
35
|
+
provider = build_provider
|
|
36
|
+
system_prompt = build_system_prompt
|
|
37
|
+
max_rounds = @agent_config['max_rounds'] || RailsConsoleAi.configuration.sub_agent_max_rounds
|
|
38
|
+
|
|
39
|
+
messages = [{ role: :user, content: @task }]
|
|
40
|
+
|
|
41
|
+
run_tool_loop(messages, system_prompt: system_prompt, tools: tools,
|
|
42
|
+
provider: provider, channel: channel, executor: executor,
|
|
43
|
+
max_rounds: max_rounds)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def run_tool_loop(messages, system_prompt:, tools:, provider:, channel:, executor:, max_rounds:)
|
|
49
|
+
result = nil
|
|
50
|
+
tool_call_counts = Hash.new(0)
|
|
51
|
+
exhausted = false
|
|
52
|
+
last_thinking = nil
|
|
53
|
+
|
|
54
|
+
max_rounds.times do |round|
|
|
55
|
+
break if channel.cancelled?
|
|
56
|
+
|
|
57
|
+
if round == 0
|
|
58
|
+
channel.display_status("Thinking...")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
begin
|
|
62
|
+
result = provider.chat_with_tools(messages, tools: tools, system_prompt: system_prompt)
|
|
63
|
+
rescue Providers::ProviderError => e
|
|
64
|
+
raise
|
|
65
|
+
end
|
|
66
|
+
@input_tokens += result.input_tokens || 0
|
|
67
|
+
@output_tokens += result.output_tokens || 0
|
|
68
|
+
|
|
69
|
+
break if channel.cancelled?
|
|
70
|
+
break unless result.tool_use?
|
|
71
|
+
|
|
72
|
+
# Display the LLM's reasoning text before executing its tool calls
|
|
73
|
+
if result.text && !result.text.strip.empty?
|
|
74
|
+
result.text.strip.split("\n").each do |line|
|
|
75
|
+
channel.display_thinking(" #{line}")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
assistant_msg = provider.format_assistant_message(result)
|
|
80
|
+
messages << assistant_msg
|
|
81
|
+
|
|
82
|
+
result.tool_calls.each do |tc|
|
|
83
|
+
break if channel.cancelled?
|
|
84
|
+
|
|
85
|
+
args_display = tc[:arguments].map { |k, v|
|
|
86
|
+
val = v.to_s
|
|
87
|
+
val = val[0, 60] + '...' if val.length > 60
|
|
88
|
+
"#{k}: #{val.inspect}"
|
|
89
|
+
}.join(', ')
|
|
90
|
+
channel.display_tool_call("#{tc[:name]}(#{args_display})")
|
|
91
|
+
|
|
92
|
+
tool_result = tools.execute(tc[:name], tc[:arguments])
|
|
93
|
+
|
|
94
|
+
preview = tool_result.to_s.lines.first(3).join.strip
|
|
95
|
+
preview = preview[0, 120] + '...' if preview.length > 120
|
|
96
|
+
channel.display_status(" #{preview}")
|
|
97
|
+
|
|
98
|
+
# Truncate large outputs to keep sub-agent context lean
|
|
99
|
+
tool_result_str = tool_result.to_s
|
|
100
|
+
if tool_result_str.length > LARGE_OUTPUT_THRESHOLD
|
|
101
|
+
tool_result_str = tool_result_str[0, LARGE_OUTPUT_PREVIEW_CHARS] +
|
|
102
|
+
"\n\n[Output truncated at #{LARGE_OUTPUT_PREVIEW_CHARS} of #{tool_result_str.length} chars]"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
tool_msg = provider.format_tool_result(tc[:id], tool_result_str)
|
|
106
|
+
messages << tool_msg
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Loop detection
|
|
110
|
+
result.tool_calls.each do |tc|
|
|
111
|
+
key = "#{tc[:name]}:#{tc[:arguments].to_json}"
|
|
112
|
+
tool_call_counts[key] += 1
|
|
113
|
+
|
|
114
|
+
if tool_call_counts[key] >= LOOP_BREAK_THRESHOLD
|
|
115
|
+
channel.display_status("Loop detected — stopping.")
|
|
116
|
+
exhausted = true
|
|
117
|
+
elsif tool_call_counts[key] >= LOOP_WARN_THRESHOLD
|
|
118
|
+
messages << { role: :user, content: "You are repeating the same tool call with the same arguments. Try a different approach or provide your answer now." }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
break if exhausted
|
|
122
|
+
|
|
123
|
+
break if executor.last_cancelled?
|
|
124
|
+
|
|
125
|
+
exhausted = true if round == max_rounds - 1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if exhausted
|
|
129
|
+
messages << { role: :user, content: "Provide your best answer now based on what you've learned." }
|
|
130
|
+
result = provider.chat(messages, system_prompt: system_prompt)
|
|
131
|
+
@input_tokens += result.input_tokens || 0
|
|
132
|
+
@output_tokens += result.output_tokens || 0
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
result&.text || '(sub-agent returned no result)'
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def build_provider
|
|
139
|
+
config = RailsConsoleAi.configuration
|
|
140
|
+
model_override = @agent_config['model'] || config.sub_agent_model
|
|
141
|
+
|
|
142
|
+
if model_override
|
|
143
|
+
config_dup = config.dup
|
|
144
|
+
config_dup.model = model_override
|
|
145
|
+
@model_used = model_override
|
|
146
|
+
Providers.build(config_dup)
|
|
147
|
+
else
|
|
148
|
+
@model_used = config.resolved_model
|
|
149
|
+
Providers.build(config)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def build_system_prompt
|
|
154
|
+
parts = []
|
|
155
|
+
parts << base_instructions
|
|
156
|
+
parts << guide_context
|
|
157
|
+
parts << pinned_memory_context
|
|
158
|
+
parts << @agent_config['body'] if @agent_config['body'] && !@agent_config['body'].strip.empty?
|
|
159
|
+
parts.compact.join("\n\n")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def base_instructions
|
|
163
|
+
<<~PROMPT.strip
|
|
164
|
+
You are a sub-agent assistant for a Ruby on Rails application. You have been delegated
|
|
165
|
+
a specific investigation task. Your job is to use the available tools to find the answer
|
|
166
|
+
efficiently, then provide a concise summary of your findings.
|
|
167
|
+
|
|
168
|
+
RULES:
|
|
169
|
+
- Focus on the specific task you were given. Do not go on tangents.
|
|
170
|
+
- Use tools to look up schema/model details rather than guessing.
|
|
171
|
+
- Prefer ActiveRecord query interface over raw SQL.
|
|
172
|
+
- Use describe_model to understand models before querying them.
|
|
173
|
+
- NEVER fabricate URLs, IDs, or data. Always look things up using model methods.
|
|
174
|
+
- When you find methods on a model (e.g. via .methods.grep), USE them rather than
|
|
175
|
+
constructing values manually.
|
|
176
|
+
- End with a concise, factual summary. Include specific IDs, values, and findings.
|
|
177
|
+
- Keep your final answer under 300 words.
|
|
178
|
+
PROMPT
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def guide_context
|
|
182
|
+
content = RailsConsoleAi.storage.read(RailsConsoleAi::GUIDE_KEY)
|
|
183
|
+
return nil if content.nil? || content.strip.empty?
|
|
184
|
+
|
|
185
|
+
"## Application Guide\n\n#{content.strip}"
|
|
186
|
+
rescue => e
|
|
187
|
+
RailsConsoleAi.logger.debug("SubAgent: guide context failed: #{e.message}")
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def pinned_memory_context
|
|
192
|
+
channel_mode = @parent_channel.respond_to?(:mode) ? @parent_channel.mode : nil
|
|
193
|
+
# For sub-agents spawned from Slack, use the Slack channel's pinned memories.
|
|
194
|
+
# For sub-agents spawned from console, use the console channel's pinned memories.
|
|
195
|
+
# Fall back to 'slack' if the parent is a sub_agent channel (nested, though we block this).
|
|
196
|
+
effective_mode = channel_mode == 'sub_agent' ? 'slack' : channel_mode
|
|
197
|
+
return nil unless effective_mode
|
|
198
|
+
|
|
199
|
+
channel_cfg = RailsConsoleAi.configuration.channels[effective_mode] || {}
|
|
200
|
+
pinned_tags = channel_cfg['pinned_memory_tags'] || []
|
|
201
|
+
return nil if pinned_tags.empty?
|
|
202
|
+
|
|
203
|
+
require 'rails_console_ai/tools/memory_tools'
|
|
204
|
+
sections = pinned_tags.filter_map do |tag|
|
|
205
|
+
content = Tools::MemoryTools.new.recall_memories(tag: tag)
|
|
206
|
+
next if content.nil? || content.include?("No memories")
|
|
207
|
+
content
|
|
208
|
+
end
|
|
209
|
+
return nil if sections.empty?
|
|
210
|
+
|
|
211
|
+
"## Key Context (from pinned memories)\n\n" + sections.join("\n\n")
|
|
212
|
+
rescue => e
|
|
213
|
+
RailsConsoleAi.logger.debug("SubAgent: pinned memory context failed: #{e.message}")
|
|
214
|
+
nil
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -37,8 +37,8 @@ module RailsConsoleAi
|
|
|
37
37
|
|
|
38
38
|
# Columns and indexes from the database table
|
|
39
39
|
begin
|
|
40
|
-
|
|
41
|
-
|
|
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].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
|
|
|
@@ -215,23 +217,130 @@ module RailsConsoleAi
|
|
|
215
217
|
end
|
|
216
218
|
|
|
217
219
|
unless @mode == :init
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
220
|
+
# Sub-agents get execute_code and read-only memory tools, but not ask_user,
|
|
221
|
+
# memory writes, skill writes, execute_plan, or delegate_task.
|
|
222
|
+
if @mode == :sub_agent
|
|
223
|
+
register_memory_recall_tools
|
|
224
|
+
register_execute_code
|
|
225
|
+
else
|
|
226
|
+
register(
|
|
227
|
+
name: 'ask_user',
|
|
228
|
+
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.',
|
|
229
|
+
parameters: {
|
|
230
|
+
'type' => 'object',
|
|
231
|
+
'properties' => {
|
|
232
|
+
'question' => { 'type' => 'string', 'description' => 'The question to ask the user' }
|
|
233
|
+
},
|
|
234
|
+
'required' => ['question']
|
|
225
235
|
},
|
|
226
|
-
|
|
236
|
+
handler: ->(args) { ask_user(args['question']) }
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
register_memory_tools
|
|
240
|
+
register_skill_tools
|
|
241
|
+
register_execute_plan
|
|
242
|
+
register_delegate_task
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def register_memory_recall_tools
|
|
248
|
+
return unless RailsConsoleAi.configuration.memories_enabled
|
|
249
|
+
|
|
250
|
+
require 'rails_console_ai/tools/memory_tools'
|
|
251
|
+
memory = MemoryTools.new
|
|
252
|
+
|
|
253
|
+
register(
|
|
254
|
+
name: 'recall_memory',
|
|
255
|
+
description: 'Retrieve a specific memory by name.',
|
|
256
|
+
parameters: {
|
|
257
|
+
'type' => 'object',
|
|
258
|
+
'properties' => {
|
|
259
|
+
'name' => { 'type' => 'string', 'description' => 'The exact memory name' }
|
|
227
260
|
},
|
|
228
|
-
|
|
229
|
-
|
|
261
|
+
'required' => ['name']
|
|
262
|
+
},
|
|
263
|
+
handler: ->(args) { memory.recall_memory(name: args['name']) }
|
|
264
|
+
)
|
|
230
265
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
266
|
+
register(
|
|
267
|
+
name: 'recall_memories',
|
|
268
|
+
description: 'Search saved memories about this codebase. Call with no args to list all, or pass a query/tag to filter.',
|
|
269
|
+
parameters: {
|
|
270
|
+
'type' => 'object',
|
|
271
|
+
'properties' => {
|
|
272
|
+
'query' => { 'type' => 'string', 'description' => 'Search term to filter by name, description, or tags' },
|
|
273
|
+
'tag' => { 'type' => 'string', 'description' => 'Filter by a specific tag' }
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
handler: ->(args) { memory.recall_memories(query: args['query'], tag: args['tag']) }
|
|
277
|
+
)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def register_execute_code
|
|
281
|
+
return unless @executor
|
|
282
|
+
|
|
283
|
+
register(
|
|
284
|
+
name: 'execute_code',
|
|
285
|
+
description: 'Execute Ruby code in the Rails console and return the result.',
|
|
286
|
+
parameters: {
|
|
287
|
+
'type' => 'object',
|
|
288
|
+
'properties' => {
|
|
289
|
+
'code' => { 'type' => 'string', 'description' => 'Ruby code to execute' }
|
|
290
|
+
},
|
|
291
|
+
'required' => ['code']
|
|
292
|
+
},
|
|
293
|
+
handler: ->(args) { execute_code(args['code']) }
|
|
294
|
+
)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def register_delegate_task
|
|
298
|
+
return unless @executor
|
|
299
|
+
|
|
300
|
+
register(
|
|
301
|
+
name: 'delegate_task',
|
|
302
|
+
description: 'Delegate an investigation to a sub-agent that runs in a separate context. ' \
|
|
303
|
+
'Use this for tasks that require multiple tool calls to figure out ' \
|
|
304
|
+
'(e.g., "find which shard user X is on", "search the codebase for how URL generation works", ' \
|
|
305
|
+
'"describe the relationships between models A, B, and C"). ' \
|
|
306
|
+
'The sub-agent runs independently and returns only a concise summary, ' \
|
|
307
|
+
'keeping this conversation\'s context small and efficient.',
|
|
308
|
+
parameters: {
|
|
309
|
+
'type' => 'object',
|
|
310
|
+
'properties' => {
|
|
311
|
+
'task' => { 'type' => 'string', 'description' => 'What to investigate. Be specific about what you need to know.' },
|
|
312
|
+
'agent' => { 'type' => 'string', 'description' => 'Optional: name of a custom agent to use (see Agents list in system prompt). Omit for general investigation.' }
|
|
313
|
+
},
|
|
314
|
+
'required' => ['task']
|
|
315
|
+
},
|
|
316
|
+
handler: ->(args) { delegate_task(args['task'], args['agent']) }
|
|
317
|
+
)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def delegate_task(task, agent_name = nil)
|
|
321
|
+
require 'rails_console_ai/sub_agent'
|
|
322
|
+
require 'rails_console_ai/agent_loader'
|
|
323
|
+
|
|
324
|
+
agent_config = nil
|
|
325
|
+
if agent_name
|
|
326
|
+
loader = AgentLoader.new
|
|
327
|
+
agent_config = loader.find_agent(agent_name)
|
|
328
|
+
unless agent_config
|
|
329
|
+
available = loader.load_all_agents.map { |a| a['name'] }
|
|
330
|
+
return "Agent not found: \"#{agent_name}\". Available agents: #{available.join(', ')}"
|
|
331
|
+
end
|
|
234
332
|
end
|
|
333
|
+
|
|
334
|
+
sub = SubAgent.new(
|
|
335
|
+
task: task,
|
|
336
|
+
agent_config: agent_config,
|
|
337
|
+
binding_context: @executor.binding_context,
|
|
338
|
+
parent_channel: @channel,
|
|
339
|
+
executor: @executor
|
|
340
|
+
)
|
|
341
|
+
result = sub.run
|
|
342
|
+
@last_sub_agent_usage = { input: sub.input_tokens, output: sub.output_tokens, model: sub.model_used }
|
|
343
|
+
"Sub-agent result (#{sub.input_tokens + sub.output_tokens} tokens used):\n#{result}"
|
|
235
344
|
end
|
|
236
345
|
|
|
237
346
|
def register_memory_tools
|
|
@@ -412,7 +521,7 @@ module RailsConsoleAi
|
|
|
412
521
|
# Show the code to the user
|
|
413
522
|
@executor.display_code_block(code)
|
|
414
523
|
|
|
415
|
-
exec_result = if @channel&.mode == 'slack' || RailsConsoleAi.configuration.auto_execute
|
|
524
|
+
exec_result = if @channel&.mode == 'slack' || @channel&.mode == 'sub_agent' || RailsConsoleAi.configuration.auto_execute
|
|
416
525
|
@executor.execute(code)
|
|
417
526
|
else
|
|
418
527
|
@executor.confirm_and_execute(code)
|
|
@@ -547,6 +656,13 @@ module RailsConsoleAi
|
|
|
547
656
|
end
|
|
548
657
|
step_report += "Return value: #{exec_result.inspect}"
|
|
549
658
|
results << step_report
|
|
659
|
+
|
|
660
|
+
# Stop on error so the LLM can fix the failed step before continuing
|
|
661
|
+
if error
|
|
662
|
+
remaining = steps.length - i - 1
|
|
663
|
+
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
|
|
664
|
+
break
|
|
665
|
+
end
|
|
550
666
|
end
|
|
551
667
|
|
|
552
668
|
results.join("\n\n")
|
|
@@ -609,6 +725,11 @@ module RailsConsoleAi
|
|
|
609
725
|
end
|
|
610
726
|
|
|
611
727
|
def register(name:, description:, parameters:, handler:)
|
|
728
|
+
# When allowed_tools is set (sub-agent with tool filter), skip tools not in the list
|
|
729
|
+
if @allowed_tools && !@allowed_tools.include?(name)
|
|
730
|
+
return
|
|
731
|
+
end
|
|
732
|
+
|
|
612
733
|
@definitions << {
|
|
613
734
|
name: name,
|
|
614
735
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
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.
|
|
4
|
+
version: 0.26.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
|