rails_console_ai 0.28.0 → 0.30.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 +45 -0
- data/README.md +48 -0
- data/app/controllers/rails_console_ai/agent_versions_controller.rb +36 -0
- data/app/controllers/rails_console_ai/agents_controller.rb +199 -0
- data/app/controllers/rails_console_ai/application_controller.rb +5 -0
- data/app/controllers/rails_console_ai/memories_controller.rb +159 -0
- data/app/controllers/rails_console_ai/memory_versions_controller.rb +33 -0
- data/app/controllers/rails_console_ai/skill_versions_controller.rb +35 -0
- data/app/controllers/rails_console_ai/skills_controller.rb +200 -0
- data/app/helpers/rails_console_ai/diff_helper.rb +114 -0
- data/app/models/rails_console_ai/agent.rb +175 -0
- data/app/models/rails_console_ai/agent_version.rb +46 -0
- data/app/models/rails_console_ai/memory.rb +98 -0
- data/app/models/rails_console_ai/memory_version.rb +46 -0
- data/app/models/rails_console_ai/session.rb +1 -1
- data/app/models/rails_console_ai/skill.rb +198 -0
- data/app/models/rails_console_ai/skill_version.rb +54 -0
- data/app/views/layouts/rails_console_ai/application.html.erb +78 -1
- data/app/views/rails_console_ai/agent_versions/index.html.erb +28 -0
- data/app/views/rails_console_ai/agent_versions/show.html.erb +25 -0
- data/app/views/rails_console_ai/agents/_form.html.erb +65 -0
- data/app/views/rails_console_ai/agents/diff.html.erb +19 -0
- data/app/views/rails_console_ai/agents/edit.html.erb +7 -0
- data/app/views/rails_console_ai/agents/index.html.erb +80 -0
- data/app/views/rails_console_ai/agents/new.html.erb +24 -0
- data/app/views/rails_console_ai/agents/show.html.erb +108 -0
- data/app/views/rails_console_ai/memories/_form.html.erb +36 -0
- data/app/views/rails_console_ai/memories/diff.html.erb +19 -0
- data/app/views/rails_console_ai/memories/edit.html.erb +7 -0
- data/app/views/rails_console_ai/memories/index.html.erb +67 -0
- data/app/views/rails_console_ai/memories/new.html.erb +23 -0
- data/app/views/rails_console_ai/memories/show.html.erb +65 -0
- data/app/views/rails_console_ai/memory_versions/index.html.erb +26 -0
- data/app/views/rails_console_ai/memory_versions/show.html.erb +21 -0
- data/app/views/rails_console_ai/skill_versions/index.html.erb +28 -0
- data/app/views/rails_console_ai/skill_versions/show.html.erb +23 -0
- data/app/views/rails_console_ai/skills/_form.html.erb +65 -0
- data/app/views/rails_console_ai/skills/diff.html.erb +22 -0
- data/app/views/rails_console_ai/skills/edit.html.erb +7 -0
- data/app/views/rails_console_ai/skills/index.html.erb +79 -0
- data/app/views/rails_console_ai/skills/new.html.erb +25 -0
- data/app/views/rails_console_ai/skills/show.html.erb +94 -0
- data/config/routes.rb +42 -0
- data/lib/rails_console_ai/agent_loader.rb +131 -43
- data/lib/rails_console_ai/agent_runner.rb +158 -0
- data/lib/rails_console_ai/channel/api.rb +139 -0
- data/lib/rails_console_ai/channel/slack.rb +33 -0
- data/lib/rails_console_ai/channel/sub_agent.rb +12 -0
- data/lib/rails_console_ai/conversation_engine.rb +50 -13
- data/lib/rails_console_ai/session_logger.rb +6 -0
- data/lib/rails_console_ai/skill_loader.rb +119 -27
- data/lib/rails_console_ai/slack_bot.rb +8 -0
- data/lib/rails_console_ai/storage/database_storage.rb +201 -0
- data/lib/rails_console_ai/sub_agent.rb +25 -0
- data/lib/rails_console_ai/tools/memory_tools.rb +102 -32
- data/lib/rails_console_ai/tools/registry.rb +99 -8
- data/lib/rails_console_ai/version.rb +1 -1
- data/lib/rails_console_ai.rb +256 -0
- data/lib/tasks/rails_console_ai.rake +7 -0
- metadata +55 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'yaml'
|
|
2
|
+
require 'rails_console_ai/storage/database_storage'
|
|
2
3
|
|
|
3
4
|
module RailsConsoleAi
|
|
4
5
|
module Tools
|
|
@@ -9,41 +10,61 @@ module RailsConsoleAi
|
|
|
9
10
|
@storage = storage || RailsConsoleAi.storage
|
|
10
11
|
end
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
frontmatter['updated_at'] = Time.now.utc.iso8601 if existing
|
|
22
|
-
|
|
23
|
-
content = "---\n#{YAML.dump(frontmatter).sub("---\n", '').strip}\n---\n\n#{description}\n"
|
|
24
|
-
@storage.write(key, content)
|
|
13
|
+
# target: :db (default) | :file
|
|
14
|
+
# Falls back to :file (with a notice in the return string) if DB tables aren't set up.
|
|
15
|
+
def save_memory(name:, description:, tags: [], target: :db, edited_by: nil, change_note: nil)
|
|
16
|
+
target = (target || :db).to_sym
|
|
17
|
+
db_fell_back = false
|
|
18
|
+
if target == :db && !Storage::DatabaseStorage.memories_available?
|
|
19
|
+
target = :file
|
|
20
|
+
db_fell_back = true
|
|
21
|
+
end
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
if target == :file
|
|
24
|
+
result = save_memory_to_file(name: name, description: description, tags: tags)
|
|
25
|
+
if db_fell_back
|
|
26
|
+
result += "\nNOTE: DB storage was requested but the rails_console_ai_memories table does not exist. " \
|
|
27
|
+
"Run `ai_db_setup` in your Rails console to enable the versioned DB store. " \
|
|
28
|
+
"Saved to a file instead."
|
|
29
|
+
end
|
|
30
|
+
result
|
|
29
31
|
else
|
|
30
|
-
|
|
32
|
+
record, was_new = Storage::DatabaseStorage.save_memory(
|
|
33
|
+
name: name, description: description, tags: tags,
|
|
34
|
+
edited_by: edited_by || 'ai', change_note: change_note
|
|
35
|
+
)
|
|
36
|
+
if was_new
|
|
37
|
+
"Memory saved (db): \"#{record.name}\" (id=#{record.id})"
|
|
38
|
+
else
|
|
39
|
+
"Memory updated (db): \"#{record.name}\" (id=#{record.id})"
|
|
40
|
+
end
|
|
31
41
|
end
|
|
32
42
|
rescue Storage::StorageError => e
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
if target == :file
|
|
44
|
+
# Preserve the original behaviour: include a hint with the raw frontmatter
|
|
45
|
+
# so the user (or AI) can paste it manually when the filesystem is read-only.
|
|
46
|
+
"FAILED to save (#{e.message}). Add this manually to .rails_console_ai/#{memory_key(name)}:\n" \
|
|
47
|
+
"---\nname: #{name}\ntags: #{Array(tags).inspect}\n---\n\n#{description}"
|
|
48
|
+
else
|
|
49
|
+
"FAILED to save (#{e.message})."
|
|
50
|
+
end
|
|
51
|
+
rescue ::ActiveRecord::RecordInvalid => e
|
|
52
|
+
"FAILED to save (#{e.message})."
|
|
35
53
|
end
|
|
36
54
|
|
|
37
55
|
def delete_memory(name:)
|
|
56
|
+
if Storage::DatabaseStorage.delete_memory_by_name(name)
|
|
57
|
+
return "Memory deleted (db): \"#{name}\""
|
|
58
|
+
end
|
|
59
|
+
|
|
38
60
|
key = memory_key(name)
|
|
39
61
|
unless @storage.exists?(key)
|
|
40
|
-
# Try to find by name match across all memory files
|
|
41
62
|
found_key = find_memory_key_by_name(name)
|
|
42
63
|
return "No memory found: \"#{name}\"" unless found_key
|
|
43
64
|
key = found_key
|
|
44
65
|
end
|
|
45
66
|
|
|
46
|
-
memory =
|
|
67
|
+
memory = load_memory_file(key)
|
|
47
68
|
@storage.delete(key)
|
|
48
69
|
"Memory deleted: \"#{memory ? memory['name'] : name}\""
|
|
49
70
|
rescue Storage::StorageError => e
|
|
@@ -51,14 +72,11 @@ module RailsConsoleAi
|
|
|
51
72
|
end
|
|
52
73
|
|
|
53
74
|
def recall_memory(name:)
|
|
54
|
-
|
|
55
|
-
memory = load_memory(key)
|
|
56
|
-
# Fall back to case-insensitive name search
|
|
57
|
-
unless memory
|
|
58
|
-
memory = load_all_memories.find { |m| m['name'].to_s.downcase == name.downcase }
|
|
59
|
-
end
|
|
75
|
+
memory = load_all_memories.find { |m| m['name'].to_s.downcase == name.to_s.downcase }
|
|
60
76
|
return "No memory found: \"#{name}\"" unless memory
|
|
61
77
|
|
|
78
|
+
record_use(memory)
|
|
79
|
+
|
|
62
80
|
line = "**#{memory['name']}**\n#{memory['description']}"
|
|
63
81
|
line += "\nTags: #{memory['tags'].join(', ')}" if memory['tags'] && !memory['tags'].empty?
|
|
64
82
|
line
|
|
@@ -88,6 +106,9 @@ module RailsConsoleAi
|
|
|
88
106
|
|
|
89
107
|
return "No memories matching your search." if results.empty?
|
|
90
108
|
|
|
109
|
+
# Every memory in the result set was loaded into the AI's context.
|
|
110
|
+
results.each { |m| record_use(m) }
|
|
111
|
+
|
|
91
112
|
results.map { |m|
|
|
92
113
|
line = "**#{m['name']}**\n#{m['description']}"
|
|
93
114
|
line += "\nTags: #{m['tags'].join(', ')}" if m['tags'] && !m['tags'].empty?
|
|
@@ -106,8 +127,44 @@ module RailsConsoleAi
|
|
|
106
127
|
}
|
|
107
128
|
end
|
|
108
129
|
|
|
130
|
+
def load_all_memories
|
|
131
|
+
db = Storage::DatabaseStorage.all_memories
|
|
132
|
+
file = load_all_file_memories
|
|
133
|
+
names = db.map { |m| m['name'].to_s.downcase }
|
|
134
|
+
file.reject! { |m| names.include?(m['name'].to_s.downcase) }
|
|
135
|
+
(db + file).sort_by { |m| m['name'].to_s.downcase }
|
|
136
|
+
end
|
|
137
|
+
|
|
109
138
|
private
|
|
110
139
|
|
|
140
|
+
# DB-backed memories only — file memories have no row to update.
|
|
141
|
+
def record_use(memory)
|
|
142
|
+
return unless memory.is_a?(Hash) && memory['source'] == :db && memory['id']
|
|
143
|
+
RailsConsoleAi::Memory.record_use!(memory['id'])
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def save_memory_to_file(name:, description:, tags:)
|
|
147
|
+
key = memory_key(name)
|
|
148
|
+
existing = load_memory_file(key)
|
|
149
|
+
|
|
150
|
+
frontmatter = {
|
|
151
|
+
'name' => name,
|
|
152
|
+
'tags' => Array(tags).empty? && existing ? (existing['tags'] || []) : Array(tags),
|
|
153
|
+
'created_at' => existing ? existing['created_at'] : Time.now.utc.iso8601
|
|
154
|
+
}
|
|
155
|
+
frontmatter['updated_at'] = Time.now.utc.iso8601 if existing
|
|
156
|
+
|
|
157
|
+
content = "---\n#{YAML.dump(frontmatter).sub("---\n", '').strip}\n---\n\n#{description}\n"
|
|
158
|
+
@storage.write(key, content)
|
|
159
|
+
|
|
160
|
+
path = @storage.respond_to?(:root_path) ? File.join(@storage.root_path, key) : key
|
|
161
|
+
if existing
|
|
162
|
+
"Memory updated: \"#{name}\" (#{path})"
|
|
163
|
+
else
|
|
164
|
+
"Memory saved: \"#{name}\" (#{path})"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
111
168
|
def memory_key(name)
|
|
112
169
|
slug = name.downcase.strip
|
|
113
170
|
.gsub(/[^a-z0-9\s-]/, '')
|
|
@@ -117,7 +174,7 @@ module RailsConsoleAi
|
|
|
117
174
|
"#{MEMORIES_DIR}/#{slug}.md"
|
|
118
175
|
end
|
|
119
176
|
|
|
120
|
-
def
|
|
177
|
+
def load_memory_file(key)
|
|
121
178
|
content = @storage.read(key)
|
|
122
179
|
return nil if content.nil? || content.strip.empty?
|
|
123
180
|
parse_memory(content)
|
|
@@ -126,26 +183,39 @@ module RailsConsoleAi
|
|
|
126
183
|
nil
|
|
127
184
|
end
|
|
128
185
|
|
|
129
|
-
def
|
|
186
|
+
def load_all_file_memories
|
|
130
187
|
keys = @storage.list("#{MEMORIES_DIR}/*.md")
|
|
131
|
-
keys.
|
|
188
|
+
keys.filter_map { |key|
|
|
189
|
+
memory = load_memory_file(key)
|
|
190
|
+
next nil unless memory
|
|
191
|
+
memory.merge('source' => :file, 'file_key' => key)
|
|
192
|
+
}
|
|
132
193
|
rescue => e
|
|
133
194
|
RailsConsoleAi.logger.warn("RailsConsoleAi: failed to load memories: #{e.message}")
|
|
134
195
|
[]
|
|
135
196
|
end
|
|
136
197
|
|
|
137
198
|
def parse_memory(content)
|
|
199
|
+
self.class.parse(content)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Public: parse a raw .md (YAML frontmatter + body) string into a hash.
|
|
203
|
+
# For memories, the body is stored under the 'description' key (memories
|
|
204
|
+
# don't have a separate description vs body — the markdown IS the memory).
|
|
205
|
+
def self.parse(content)
|
|
138
206
|
return nil unless content =~ /\A---\s*\n(.*?\n)---\s*\n(.*)/m
|
|
139
207
|
frontmatter = YAML.safe_load($1, permitted_classes: [Time, Date]) || {}
|
|
140
208
|
description = $2.strip
|
|
141
209
|
frontmatter.merge('description' => description)
|
|
210
|
+
rescue Psych::SyntaxError
|
|
211
|
+
nil
|
|
142
212
|
end
|
|
143
213
|
|
|
144
214
|
def find_memory_key_by_name(name)
|
|
145
215
|
keys = @storage.list("#{MEMORIES_DIR}/*.md")
|
|
146
216
|
keys.find do |key|
|
|
147
|
-
memory =
|
|
148
|
-
memory && memory['name'].to_s.downcase == name.downcase
|
|
217
|
+
memory = load_memory_file(key)
|
|
218
|
+
memory && memory['name'].to_s.downcase == name.to_s.downcase
|
|
149
219
|
end
|
|
150
220
|
end
|
|
151
221
|
end
|
|
@@ -6,7 +6,7 @@ module RailsConsoleAi
|
|
|
6
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 delegate_task explore_output].freeze
|
|
9
|
+
NO_CACHE = %w[ask_user save_memory delete_memory recall_memory execute_code execute_plan activate_skill save_skill delete_skill save_agent delete_agent delegate_task explore_output].freeze
|
|
10
10
|
|
|
11
11
|
def initialize(executor: nil, mode: :default, channel: nil, allowed_tools: nil)
|
|
12
12
|
@executor = executor
|
|
@@ -254,6 +254,7 @@ module RailsConsoleAi
|
|
|
254
254
|
|
|
255
255
|
register_memory_tools
|
|
256
256
|
register_skill_tools
|
|
257
|
+
register_agent_tools
|
|
257
258
|
register_execute_plan
|
|
258
259
|
register_delegate_task
|
|
259
260
|
end
|
|
@@ -381,9 +382,20 @@ module RailsConsoleAi
|
|
|
381
382
|
loader = AgentLoader.new
|
|
382
383
|
agent_config = loader.find_agent(agent_name)
|
|
383
384
|
unless agent_config
|
|
384
|
-
|
|
385
|
+
# Distinguish "doesn't exist" from "exists but isn't approved yet".
|
|
386
|
+
proposed = loader.find_any_agent(agent_name)
|
|
387
|
+
if proposed && proposed['source'] == :db && proposed['status'] != 'approved'
|
|
388
|
+
return "Agent \"#{agent_name}\" exists but is awaiting human approval and cannot be invoked yet. " \
|
|
389
|
+
"Ask the user to approve it in the web UI at /rails_console_ai/agents."
|
|
390
|
+
end
|
|
391
|
+
available = loader.load_activatable_agents.map { |a| a['name'] }
|
|
385
392
|
return "Agent not found: \"#{agent_name}\". Available agents: #{available.join(', ')}"
|
|
386
393
|
end
|
|
394
|
+
|
|
395
|
+
# Usage tracking — DB-backed agents only.
|
|
396
|
+
if agent_config['source'] == :db && agent_config['id']
|
|
397
|
+
RailsConsoleAi::Agent.record_use!(agent_config['id'])
|
|
398
|
+
end
|
|
387
399
|
end
|
|
388
400
|
|
|
389
401
|
sub = SubAgent.new(
|
|
@@ -406,18 +418,26 @@ module RailsConsoleAi
|
|
|
406
418
|
|
|
407
419
|
register(
|
|
408
420
|
name: 'save_memory',
|
|
409
|
-
description: 'Save a fact or pattern you learned about this codebase for future sessions. Use after discovering how something works (e.g. sharding, auth, custom business logic).',
|
|
421
|
+
description: 'Save a fact or pattern you learned about this codebase for future sessions. Use after discovering how something works (e.g. sharding, auth, custom business logic). Defaults to the versioned DB store; pass target: "file" to write to the on-disk .rails_console_ai/memories directory instead.',
|
|
410
422
|
parameters: {
|
|
411
423
|
'type' => 'object',
|
|
412
424
|
'properties' => {
|
|
413
425
|
'name' => { 'type' => 'string', 'description' => 'Short name for this memory (e.g. "Sharding architecture")' },
|
|
414
426
|
'description' => { 'type' => 'string', 'description' => 'Detailed description of what you learned' },
|
|
415
|
-
'tags' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Optional tags (e.g. ["database", "sharding"])' }
|
|
427
|
+
'tags' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Optional tags (e.g. ["database", "sharding"])' },
|
|
428
|
+
'target' => { 'type' => 'string', 'enum' => ['db', 'file'], 'description' => 'Where to store this memory. "db" (default) is versioned and editable via the web UI; "file" writes a Markdown file under .rails_console_ai/memories/.' },
|
|
429
|
+
'change_note' => { 'type' => 'string', 'description' => 'Optional one-line note describing this edit (DB store only).' }
|
|
416
430
|
},
|
|
417
431
|
'required' => ['name', 'description']
|
|
418
432
|
},
|
|
419
433
|
handler: ->(args) {
|
|
420
|
-
memory.save_memory(
|
|
434
|
+
memory.save_memory(
|
|
435
|
+
name: args['name'],
|
|
436
|
+
description: args['description'],
|
|
437
|
+
tags: args['tags'] || [],
|
|
438
|
+
target: (args['target'] || 'db').to_sym,
|
|
439
|
+
change_note: args['change_note']
|
|
440
|
+
)
|
|
421
441
|
}
|
|
422
442
|
)
|
|
423
443
|
|
|
@@ -480,19 +500,30 @@ module RailsConsoleAi
|
|
|
480
500
|
handler: ->(args) {
|
|
481
501
|
skill = loader.find_skill(args['name'])
|
|
482
502
|
unless skill
|
|
503
|
+
# Distinguish "doesn't exist" from "exists but isn't approved yet".
|
|
504
|
+
proposed = loader.find_any_skill(args['name'])
|
|
505
|
+
if proposed && proposed['source'] == :db && proposed['status'] != 'approved'
|
|
506
|
+
return "Skill \"#{args['name']}\" exists but is awaiting human approval and cannot be activated yet. " \
|
|
507
|
+
"Ask the user to approve it in the web UI at /rails_console_ai/skills."
|
|
508
|
+
end
|
|
483
509
|
return "Skill not found: \"#{args['name']}\". Use the skills listed in the system prompt."
|
|
484
510
|
end
|
|
485
511
|
|
|
486
512
|
bypass_methods = Array(skill['bypass_guards_for_methods'])
|
|
487
513
|
@executor.activate_skill_bypasses(bypass_methods) unless bypass_methods.empty?
|
|
488
514
|
|
|
515
|
+
# Usage tracking — DB-backed skills only (file skills have no row to update).
|
|
516
|
+
if skill['source'] == :db && skill['id']
|
|
517
|
+
RailsConsoleAi::Skill.record_use!(skill['id'])
|
|
518
|
+
end
|
|
519
|
+
|
|
489
520
|
skill['body']
|
|
490
521
|
}
|
|
491
522
|
)
|
|
492
523
|
|
|
493
524
|
register(
|
|
494
525
|
name: 'save_skill',
|
|
495
|
-
description: 'Create or update a skill — a reusable procedure for a specific operation. Use when the user asks you to create a skill, recipe, or runbook. Skills differ from memories: a skill is a step-by-step procedure to follow, while a memory is a fact or pattern you learned.',
|
|
526
|
+
description: 'Create or update a skill — a reusable procedure for a specific operation. Use when the user asks you to create a skill, recipe, or runbook. Skills differ from memories: a skill is a step-by-step procedure to follow, while a memory is a fact or pattern you learned. Defaults to the versioned DB store; pass target: "file" to write to the on-disk .rails_console_ai/skills directory instead. IMPORTANT: skills saved to the DB start in "proposed" state and must be approved by a human in the web UI before you can activate them. Edits to an approved skill also revert it to proposed. Tell the user to visit /rails_console_ai/skills to approve.',
|
|
496
527
|
parameters: {
|
|
497
528
|
'type' => 'object',
|
|
498
529
|
'properties' => {
|
|
@@ -500,7 +531,9 @@ module RailsConsoleAi
|
|
|
500
531
|
'description' => { 'type' => 'string', 'description' => 'One-line description of when to use this skill' },
|
|
501
532
|
'body' => { 'type' => 'string', 'description' => 'The full skill recipe in markdown. Include: ## When to use, ## Recipe (numbered steps with code blocks), ## Notes (optional).' },
|
|
502
533
|
'tags' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Optional tags for categorization (e.g. ["booking-page", "admin"])' },
|
|
503
|
-
'bypass_guards_for_methods' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Methods that should bypass safety guards when this skill is active (e.g. ["BookingPage#save!", "BookingPage#ensure_subdomain_set!"])' }
|
|
534
|
+
'bypass_guards_for_methods' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Methods that should bypass safety guards when this skill is active (e.g. ["BookingPage#save!", "BookingPage#ensure_subdomain_set!"])' },
|
|
535
|
+
'target' => { 'type' => 'string', 'enum' => ['db', 'file'], 'description' => 'Where to store this skill. "db" (default) is versioned and editable via the web UI; "file" writes a Markdown file under .rails_console_ai/skills/.' },
|
|
536
|
+
'change_note' => { 'type' => 'string', 'description' => 'Optional one-line note describing this edit (DB store only).' }
|
|
504
537
|
},
|
|
505
538
|
'required' => %w[name description body]
|
|
506
539
|
},
|
|
@@ -510,7 +543,9 @@ module RailsConsoleAi
|
|
|
510
543
|
description: args['description'],
|
|
511
544
|
body: args['body'],
|
|
512
545
|
tags: args['tags'] || [],
|
|
513
|
-
bypass_guards_for_methods: args['bypass_guards_for_methods'] || []
|
|
546
|
+
bypass_guards_for_methods: args['bypass_guards_for_methods'] || [],
|
|
547
|
+
target: (args['target'] || 'db').to_sym,
|
|
548
|
+
change_note: args['change_note']
|
|
514
549
|
)
|
|
515
550
|
}
|
|
516
551
|
)
|
|
@@ -529,6 +564,62 @@ module RailsConsoleAi
|
|
|
529
564
|
)
|
|
530
565
|
end
|
|
531
566
|
|
|
567
|
+
def register_agent_tools
|
|
568
|
+
return unless @executor
|
|
569
|
+
|
|
570
|
+
require 'rails_console_ai/agent_loader'
|
|
571
|
+
loader = RailsConsoleAi::AgentLoader.new
|
|
572
|
+
|
|
573
|
+
register(
|
|
574
|
+
name: 'save_agent',
|
|
575
|
+
description: 'Create or update a sub-agent definition — a reusable specialist that the main assistant can invoke via delegate_task. ' \
|
|
576
|
+
'Use when the user asks you to create a new agent, recipe-agent, or sub-task specialist. ' \
|
|
577
|
+
'Agents differ from skills: an agent runs in its own sub-conversation with a constrained tool set, while a skill is a procedure followed inline. ' \
|
|
578
|
+
'Defaults to the versioned DB store; pass target: "file" to write to the on-disk .rails_console_ai/agents directory instead. ' \
|
|
579
|
+
'IMPORTANT: agents saved to the DB start in "proposed" state and must be approved by a human in the web UI before delegate_task can invoke them. ' \
|
|
580
|
+
'Edits to an approved agent also revert it to proposed. Tell the user to visit /rails_console_ai/agents to approve.',
|
|
581
|
+
parameters: {
|
|
582
|
+
'type' => 'object',
|
|
583
|
+
'properties' => {
|
|
584
|
+
'name' => { 'type' => 'string', 'description' => 'Agent name (e.g. "Investigate billing"), shown in the Agents list and used as the delegate_task `agent` parameter' },
|
|
585
|
+
'description' => { 'type' => 'string', 'description' => 'One-line description of what this agent specializes in' },
|
|
586
|
+
'body' => { 'type' => 'string', 'description' => 'The agent\'s system instructions in markdown. Include: persona, strategy, rules, expected output format.' },
|
|
587
|
+
'max_rounds' => { 'type' => 'integer', 'description' => 'Optional: maximum tool-loop iterations for the sub-agent (defaults to global config). Use a smaller number for tightly-scoped agents.' },
|
|
588
|
+
'model' => { 'type' => 'string', 'description' => 'Optional: model override for this agent (e.g. "claude-haiku-4" for cheap fast agents)' },
|
|
589
|
+
'tools' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Optional: whitelist of tool names the sub-agent is allowed to call. Omit to allow the default set.' },
|
|
590
|
+
'target' => { 'type' => 'string', 'enum' => ['db', 'file'], 'description' => 'Where to store this agent. "db" (default) is versioned and editable via the web UI; "file" writes a Markdown file under .rails_console_ai/agents/.' },
|
|
591
|
+
'change_note' => { 'type' => 'string', 'description' => 'Optional one-line note describing this edit (DB store only).' }
|
|
592
|
+
},
|
|
593
|
+
'required' => %w[name description body]
|
|
594
|
+
},
|
|
595
|
+
handler: ->(args) {
|
|
596
|
+
loader.save_agent(
|
|
597
|
+
name: args['name'],
|
|
598
|
+
description: args['description'],
|
|
599
|
+
body: args['body'],
|
|
600
|
+
max_rounds: args['max_rounds'],
|
|
601
|
+
model: args['model'],
|
|
602
|
+
tools: args['tools'] || [],
|
|
603
|
+
target: (args['target'] || 'db').to_sym,
|
|
604
|
+
change_note: args['change_note']
|
|
605
|
+
)
|
|
606
|
+
}
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
register(
|
|
610
|
+
name: 'delete_agent',
|
|
611
|
+
description: 'Delete a sub-agent by name. Built-in (gem-shipped) agents cannot be deleted; this tool will tell you that and suggest creating a same-named override instead.',
|
|
612
|
+
parameters: {
|
|
613
|
+
'type' => 'object',
|
|
614
|
+
'properties' => {
|
|
615
|
+
'name' => { 'type' => 'string', 'description' => 'The agent name to delete' }
|
|
616
|
+
},
|
|
617
|
+
'required' => ['name']
|
|
618
|
+
},
|
|
619
|
+
handler: ->(args) { loader.delete_agent(name: args['name']) }
|
|
620
|
+
)
|
|
621
|
+
end
|
|
622
|
+
|
|
532
623
|
def register_execute_plan
|
|
533
624
|
return unless @executor
|
|
534
625
|
|