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).
Constant Summary collapse
- RESERVED_AGENT_NAMES =
--- named agents (app/agents/
/ — the staff pattern) --------------- %w[agent shared].freeze
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.
- #named_agent_dirs ⇒ Object
-
#named_agent_scopes ⇒ Object
{ "clerk" => AgentScope, ... }.
- #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 24 |
# 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.named_agent_scopes = -> { registry.named_agent_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).
71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/silas/registry.rb', line 71 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? if Silas.memory_enabled? b["remember"] = Silas::Tools::Remember b["recall"] = Silas::Tools::Recall end b["handoff"] = Silas::Tools::Handoff if named_agent_dirs.any? 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.
59 60 61 62 63 64 65 66 67 |
# File 'lib/silas/registry.rb', line 59 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).
85 86 87 |
# File 'lib/silas/registry.rb', line 85 def connections @connections ||= Silas::Connections.new(root: @root, client_factory: Silas.config.mcp_client_factory).warm! end |
#definitions ⇒ Object
98 99 100 |
# File 'lib/silas/registry.rb', line 98 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.
105 106 107 108 109 110 |
# File 'lib/silas/registry.rb', line 105 def digest Digest::SHA256.hexdigest(JSON.generate({ tools: definitions, skills: skills.map { |s| [ s.name, s.description ] } })) end |
#named_agent_dirs ⇒ Object
116 117 118 |
# File 'lib/silas/registry.rb', line 116 def named_agent_dirs @named_agent_dirs ||= Dir[@root.join("app/agents/*")].select { |p| File.directory?(p) }.sort end |
#named_agent_scopes ⇒ Object
{ "clerk" => AgentScope, ... }. Each named agent is a full top-level
agent: its own instructions.md, agent.yml, tools/, skills/ — autoloaded
under Agents::
124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/silas/registry.rb', line 124 def named_agent_scopes @named_agent_scopes ||= named_agent_dirs.to_h do |dir| name = File.basename(dir) if RESERVED_AGENT_NAMES.include?(name) raise Error, "app/agents/#{name} collides with a reserved name — " \ "'agent' is the root app/agent; rename the directory" end [ name, build_agent_scope(Pathname(dir), name, const_base: "Agents::#{name.camelize}", run_code: Silas.sandbox_enabled?, named: true) ] end end |
#resolver ⇒ Object
89 90 91 92 93 94 95 96 |
# File 'lib/silas/registry.rb', line 89 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.
52 53 54 55 |
# File 'lib/silas/registry.rb', line 52 def schedules @schedules ||= Dir[@root.join("app/agent/schedules/**/*.{md,rb}")].sort .map { |f| Schedule.parse(Pathname(f), root: @root) } end |
#skills ⇒ Object
45 46 47 |
# File 'lib/silas/registry.rb', line 45 def skills @skills ||= Dir[@root.join("app/agent/skills/*.md")].sort.map { |f| Skill.parse(f) } end |
#subagent_dirs ⇒ Object
--- subagents -----------------------------------------------------------
139 140 141 |
# File 'lib/silas/registry.rb', line 139 def subagent_dirs @subagent_dirs ||= Dir[@root.join("app/agent/subagents/*")].select { |p| File.directory?(p) }.sort end |
#subagent_index ⇒ Object
143 144 145 146 147 148 |
# File 'lib/silas/registry.rb', line 143 def subagent_index subagent_dirs.map do |dir| name = File.basename(dir) [ name, subagent_agent(dir, name).description ] end end |
#subagent_scopes ⇒ Object
150 151 152 153 154 155 |
# File 'lib/silas/registry.rb', line 150 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::
32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/silas/registry.rb', line 32 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 |