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), skill description, or final_answer schema changes.
-
#initialize(root: Rails.root) ⇒ Registry
constructor
A new instance of Registry.
- #named_agent_dirs ⇒ Object
-
#named_agent_scopes ⇒ Object
{ "clerk" => AgentScope, ... }.
- #resolver ⇒ Object
- #root_agent ⇒ 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).
73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/silas/registry.rb', line 73 def builtins b = {} b["ask_question"] = Silas::Tools::AskQuestion if Silas.config.ask_question 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.
61 62 63 64 65 66 67 68 69 |
# File 'lib/silas/registry.rb', line 61 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).
88 89 90 |
# File 'lib/silas/registry.rb', line 88 def connections @connections ||= Silas::Connections.new(root: @root, client_factory: Silas.config.mcp_client_factory).warm! end |
#definitions ⇒ Object
101 102 103 |
# File 'lib/silas/registry.rb', line 101 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), skill description, or final_answer schema changes.
final_answer is appended ONLY when present: schema-less agents keep a byte-identical digest across upgrades, so turns parked over a deploy never fail NondeterminismError for a key they don't use.
112 113 114 115 116 |
# File 'lib/silas/registry.rb', line 112 def digest payload = { tools: definitions, skills: skills.map { |s| [ s.name, s.description ] } } payload[:final_answer] = root_agent.final_answer if root_agent.final_answer.present? Digest::SHA256.hexdigest(JSON.generate(payload)) end |
#named_agent_dirs ⇒ Object
126 127 128 |
# File 'lib/silas/registry.rb', line 126 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::
134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/silas/registry.rb', line 134 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
92 93 94 95 96 97 98 99 |
# File 'lib/silas/registry.rb', line 92 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 |
#root_agent ⇒ Object
118 119 120 |
# File 'lib/silas/registry.rb', line 118 def root_agent @root_agent ||= Silas::Agent.load(root: @root) 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 56 57 |
# File 'lib/silas/registry.rb', line 52 def schedules @schedules ||= ( Dir[@root.join("app/agent/schedules/**/*.{md,rb}")].sort + Dir[@root.join("app/agents/*/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 -----------------------------------------------------------
149 150 151 |
# File 'lib/silas/registry.rb', line 149 def subagent_dirs @subagent_dirs ||= Dir[@root.join("app/agent/subagents/*")].select { |p| File.directory?(p) }.sort end |
#subagent_index ⇒ Object
153 154 155 156 157 158 |
# File 'lib/silas/registry.rb', line 153 def subagent_index subagent_dirs.map do |dir| name = File.basename(dir) [ name, subagent_agent(dir, name).description ] end end |
#subagent_scopes ⇒ Object
160 161 162 163 164 165 |
# File 'lib/silas/registry.rb', line 160 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 |