Class: Silas::Registry
- Inherits:
-
Object
- Object
- Silas::Registry
- Defined in:
- lib/silas/registry.rb
Overview
Discovery by directory convention — no registration. Globs app/agent/, resolves tool classes through Zeitwerk, validates them at boot, and computes the definitions digest that guards against a deploy changing the agent mid-turn (NondeterminismError).
Class Method Summary collapse
Instance Method Summary collapse
-
#builtins ⇒ Object
Built-in harness tools: load_skill when skills exist; delegate when subagents exist (root only — subagents never get delegate: depth-1).
-
#channels ⇒ Object
name => Channel subclass.
-
#connections ⇒ Object
Remote MCP tools from app/agent/connections/*.yml (warmed once at boot).
- #definitions ⇒ Object
-
#digest ⇒ Object
Stable across boots for the same agent definition; changes when any tool schema (incl. the delegate roster + remote connection tools), or skill description, changes.
-
#initialize(root: Rails.root) ⇒ Registry
constructor
A new instance of Registry.
- #resolver ⇒ Object
-
#schedules ⇒ Object
Schedules by filesystem identity (subdirs included).
- #skills ⇒ Object
-
#subagent_dirs ⇒ Object
--- subagents -----------------------------------------------------------.
- #subagent_index ⇒ Object
- #subagent_scopes ⇒ Object
-
#tools ⇒ Object
tool_name => Class.
Constructor Details
Class Method Details
.install!(root: Rails.root) ⇒ Object
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# File 'lib/silas/registry.rb', line 9 def self.install!(root: Rails.root) registry = new(root: root) Silas.config.tool_resolver = registry.resolver Silas.config.tool_definitions = -> { registry.definitions } Silas.config.definitions_digest = -> { registry.digest } Silas.config.skills = -> { registry.skills } Silas.config.schedules = -> { registry.schedules } Silas.config.channel_resolver = ->(name) { registry.channels[name] } Silas.config.subagent_index = -> { registry.subagent_index } Silas.config.subagent_scopes = -> { registry.subagent_scopes } Silas.config.agent_override = nil Silas.config.instructions_dir = nil Silas.reset_agent_memo! registry end |
Instance Method Details
#builtins ⇒ Object
Built-in harness tools: load_skill when skills exist; delegate when subagents exist (root only — subagents never get delegate: depth-1).
70 71 72 73 74 75 76 |
# File 'lib/silas/registry.rb', line 70 def builtins b = {} b["load_skill"] = Silas::Tools::LoadSkill if skills.any? b["delegate"] = Silas::Tools::Delegate if subagent_dirs.any? b["run_code"] = Silas::Tools::RunCode if Silas.sandbox_enabled? b end |
#channels ⇒ Object
name => Channel subclass. Filename identity, like tools. Also not in the digest — a channel is a trigger/transport, not a model-visible capability.
58 59 60 61 62 63 64 65 66 |
# File 'lib/silas/registry.rb', line 58 def channels @channels ||= Dir[@root.join("app/agent/channels/*.rb")].sort.to_h do |file| name = File.basename(file, ".rb") klass = "Agent::Channels::#{name.camelize}".constantize raise Error, "#{klass} (from #{file}) must inherit from Silas::Channel" unless klass < Silas::Channel [ name, klass ] end end |
#connections ⇒ Object
Remote MCP tools from app/agent/connections/*.yml (warmed once at boot).
79 80 81 |
# File 'lib/silas/registry.rb', line 79 def connections @connections ||= Silas::Connections.new(root: @root, client_factory: Silas.config.mcp_client_factory).warm! end |
#definitions ⇒ Object
92 93 94 |
# File 'lib/silas/registry.rb', line 92 def definitions (tools.values + builtins.values).map(&:schema) + connections.definitions end |
#digest ⇒ Object
Stable across boots for the same agent definition; changes when any tool schema (incl. the delegate roster + remote connection tools), or skill description, changes.
99 100 101 102 103 104 |
# File 'lib/silas/registry.rb', line 99 def digest Digest::SHA256.hexdigest(JSON.generate({ tools: definitions, skills: skills.map { |s| [ s.name, s.description ] } })) end |
#resolver ⇒ Object
83 84 85 86 87 88 89 90 |
# File 'lib/silas/registry.rb', line 83 def resolver lambda do |name| klass = tools[name] || builtins[name] return klass.new if klass connections.resolve(name) or raise Error, "unknown tool #{name.inspect}" end end |
#schedules ⇒ Object
Schedules by filesystem identity (subdirs included). Sorted so identity and compile order are deterministic. Deliberately NOT in #definitions or #digest — a schedule is a trigger, not a model-visible capability.
51 52 53 54 |
# File 'lib/silas/registry.rb', line 51 def schedules @schedules ||= Dir[@root.join("app/agent/schedules/**/*.{md,rb}")].sort .map { |f| Schedule.parse(Pathname(f), root: @root) } end |
#skills ⇒ Object
44 45 46 |
# File 'lib/silas/registry.rb', line 44 def skills @skills ||= Dir[@root.join("app/agent/skills/*.md")].sort.map { |f| Skill.parse(f) } end |
#subagent_dirs ⇒ Object
--- subagents -----------------------------------------------------------
108 109 110 |
# File 'lib/silas/registry.rb', line 108 def subagent_dirs @subagent_dirs ||= Dir[@root.join("app/agent/subagents/*")].select { |p| File.directory?(p) }.sort end |
#subagent_index ⇒ Object
112 113 114 115 116 117 |
# File 'lib/silas/registry.rb', line 112 def subagent_index subagent_dirs.map do |dir| name = File.basename(dir) [ name, subagent_agent(dir, name).description ] end end |
#subagent_scopes ⇒ Object
119 120 121 122 123 124 |
# File 'lib/silas/registry.rb', line 119 def subagent_scopes @subagent_scopes ||= subagent_dirs.to_h do |dir| name = File.basename(dir) [ name, build_subagent_scope(Pathname(dir), name) ] end end |
#tools ⇒ Object
tool_name => Class. Filename is identity; the class must match Zeitwerk's
expectation for it (Agent::Tools::
31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/silas/registry.rb', line 31 def tools @tools ||= Dir[@root.join("app/agent/tools/*.rb")].sort.to_h do |file| name = File.basename(file, ".rb") klass = "Agent::Tools::#{name.camelize}".constantize unless klass.ancestors.include?(Silas::Tool) raise Error, "#{klass} (from #{file}) must inherit from Silas::Tool" end klass.validate_signature! [ name, klass ] end end |