Class: Silas::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/silas/configuration.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConfiguration

Returns a new instance of Configuration.



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/silas/configuration.rb', line 114

def initialize
  @adapter = :ruby_llm
  # Must be resolvable by the installed ruby_llm's model registry — newer
  # Claude models may need `RubyLLM.models.refresh!` before they resolve.
  # (Sonnet 4.5 ships in every supported registry; never default a first
  # run to the priciest model.)
  @default_model = "claude-sonnet-4-5"
  @queue_name = :default
  @around_model_call = nil
  @approval_ttl = 7.days
  @max_steps = 25
  @compact_at = 0.9
  @isolate_steps = true
  @tool_resolver = nil
  @tool_definitions = nil
  @definitions_digest = nil
  @skills = nil
  @schedules = nil
  @subagent_index = nil
  @subagent_scopes = nil
  @agent_override = nil
  @instructions_dir = nil
  @channel_resolver = nil
  @slack_signing_secret = nil
  @slack_bot_token = nil
  @mcp_server_host = "127.0.0.1"
  @eval_dir = "test/agent_evals"
  @eval_grader = nil
  @sandbox = :none
  @memory = true
  @memory_approval = :always
  @memory_injection_limit = 8
  @ask_question = true
  @sandbox_image = nil
  @sandbox_network = "none"
  @sandbox_memory = "512m"
  @sandbox_cpus = "1"
  @sandbox_pids = 256
  @sandbox_workdir = "/workspace"
  @sandbox_docker_bin = "docker"
  @sandbox_timeout = 30
  @api_auth = ->(controller) { controller.head :not_found } # deny by default
  @api_actor = ->(_controller) { "api" }
  @api_stream_poll_interval = 0.5
  @api_stream_max_duration = 300
  @inbox_auth = ->(controller) { controller.head :not_found } # deny by default
  @inbox_public_read = false
  @inbox_actor = ->(controller) { controller.try(:current_user)&.try(:email) || "inbox" }
  # OVERRIDE map only — pricing comes from RubyLLM's model registry
  # (1,100+ models, refreshed upstream from models.dev). List a model here
  # to beat the registry: fine-tunes, custom deployments, models newer
  # than the installed registry. Units: per 1k tokens, 1e6 units = $1.
  @model_prices = {}
end

Instance Attribute Details

#adapterObject

Inference adapter seam: :ruby_llm, or any object responding to #execute_step. (Named engine before 0.4 — see the deprecated alias below; "engine" already meant the Rails engine at Silas::Engine.)



6
7
8
# File 'lib/silas/configuration.rb', line 6

def adapter
  @adapter
end

#agent_overrideObject

Subagents: the roster (name+description) and per-name scope builders, plus the active-agent overrides swapped in during a nested run.



36
37
38
# File 'lib/silas/configuration.rb', line 36

def agent_override
  @agent_override
end

#api_actorObject

config.auth and the agent_sdk_* options were removed with the :agent_sdk adapter in 0.2 (warning no-ops for one release) and hard-removed in 0.3 — a leftover write now raises NoMethodError. Delete them from your initializer. JSON API (mounted under /silas/api/v1). api_auth — deny-by-default lambda, same contract as inbox_auth: the host DENIES by rendering (or head-ing) and PASSES by not rendering. Wire a token check, Devise, whatever you run. api_actor — controller -> identity string recorded on approvals made through the API (approved_by / declined by). api_stream_poll_interval — seconds between SSE row polls. api_stream_max_duration — seconds before an SSE stream closes itself (clients reconnect with Last-Event-ID); bounds thread hold.



84
85
86
# File 'lib/silas/configuration.rb', line 84

def api_actor
  @api_actor
end

#api_authObject

config.auth and the agent_sdk_* options were removed with the :agent_sdk adapter in 0.2 (warning no-ops for one release) and hard-removed in 0.3 — a leftover write now raises NoMethodError. Delete them from your initializer. JSON API (mounted under /silas/api/v1). api_auth — deny-by-default lambda, same contract as inbox_auth: the host DENIES by rendering (or head-ing) and PASSES by not rendering. Wire a token check, Devise, whatever you run. api_actor — controller -> identity string recorded on approvals made through the API (approved_by / declined by). api_stream_poll_interval — seconds between SSE row polls. api_stream_max_duration — seconds before an SSE stream closes itself (clients reconnect with Last-Event-ID); bounds thread hold.



84
85
86
# File 'lib/silas/configuration.rb', line 84

def api_auth
  @api_auth
end

#api_stream_max_durationObject

config.auth and the agent_sdk_* options were removed with the :agent_sdk adapter in 0.2 (warning no-ops for one release) and hard-removed in 0.3 — a leftover write now raises NoMethodError. Delete them from your initializer. JSON API (mounted under /silas/api/v1). api_auth — deny-by-default lambda, same contract as inbox_auth: the host DENIES by rendering (or head-ing) and PASSES by not rendering. Wire a token check, Devise, whatever you run. api_actor — controller -> identity string recorded on approvals made through the API (approved_by / declined by). api_stream_poll_interval — seconds between SSE row polls. api_stream_max_duration — seconds before an SSE stream closes itself (clients reconnect with Last-Event-ID); bounds thread hold.



84
85
86
# File 'lib/silas/configuration.rb', line 84

def api_stream_max_duration
  @api_stream_max_duration
end

#api_stream_poll_intervalObject

config.auth and the agent_sdk_* options were removed with the :agent_sdk adapter in 0.2 (warning no-ops for one release) and hard-removed in 0.3 — a leftover write now raises NoMethodError. Delete them from your initializer. JSON API (mounted under /silas/api/v1). api_auth — deny-by-default lambda, same contract as inbox_auth: the host DENIES by rendering (or head-ing) and PASSES by not rendering. Wire a token check, Devise, whatever you run. api_actor — controller -> identity string recorded on approvals made through the API (approved_by / declined by). api_stream_poll_interval — seconds between SSE row polls. api_stream_max_duration — seconds before an SSE stream closes itself (clients reconnect with Last-Event-ID); bounds thread hold.



84
85
86
# File 'lib/silas/configuration.rb', line 84

def api_stream_poll_interval
  @api_stream_poll_interval
end

#approval_ttlObject

How long a parked approval (or in-doubt invocation) waits before expiring. eve parks forever; Silas expires (parked-forever ghosts are a bug, not a feature).



16
17
18
# File 'lib/silas/configuration.rb', line 16

def approval_ttl
  @approval_ttl
end

#around_model_callObject

Optional hook wrapping every model call — e.g. ruby_llm-resilience:

config.around_model_call = ->(ctx, &call) { RubyLLM::Resilience.chain(:anthropic) { call.() } }


13
14
15
# File 'lib/silas/configuration.rb', line 13

def around_model_call
  @around_model_call
end

#ask_questionObject

The ask_question builtin (agent parks to ask the operator something; answered from the inbox/API). false removes it from the toolset — note that adding/removing a builtin changes the definitions digest, so turns PARKED across that change fail loudly on resume (the nondeterminism guard working as designed). Settle parked turns before flipping this.



49
50
51
# File 'lib/silas/configuration.rb', line 49

def ask_question
  @ask_question
end

#channel_resolverObject

Channels: name -> Channel subclass (wired by the Registry). Slack creds default to credentials.dig(:silas, :slack, ...); nil disables Slack.



52
53
54
# File 'lib/silas/configuration.rb', line 52

def channel_resolver
  @channel_resolver
end

#compact_atObject

Context compaction trigger. A Float in (0, 1] compacts when the measured context passes that fraction of the model's registry context window (models the registry doesn't know are never compacted). An Integer is an absolute token threshold — the form custom adapters need, since they have no registry entry. nil/false disables. Compaction summarises PRIOR turns into a persisted row (exactly-once, replay-deterministic); the current turn is never compacted.



26
27
28
# File 'lib/silas/configuration.rb', line 26

def compact_at
  @compact_at
end

#default_modelObject

Default model when agent.yml doesn't specify one.



8
9
10
# File 'lib/silas/configuration.rb', line 8

def default_model
  @default_model
end

#definitions_digestObject

Injectable seams until/beyond the Registry: name -> tool object, the tool schema list for the model, and the definitions digest for the nondeterminism guard. The Registry wires real defaults at boot.



33
34
35
# File 'lib/silas/configuration.rb', line 33

def definitions_digest
  @definitions_digest
end

#eval_dirObject

Evals: where *_eval.rb scenarios live, and an optional custom LLM grader.



98
99
100
# File 'lib/silas/configuration.rb', line 98

def eval_dir
  @eval_dir
end

#eval_graderObject

Evals: where *_eval.rb scenarios live, and an optional custom LLM grader.



98
99
100
# File 'lib/silas/configuration.rb', line 98

def eval_grader
  @eval_grader
end

#inbox_actorObject

Inbox (mountable UI at /silas/inbox). inbox_auth — deny-by-default lambda; the host renders/head-404s to DENY and passes by NOT rendering (resilience pattern). inbox_public_read — reads render for anyone; approve/decline still gated. inbox_actor — controller -> identity string (approved_by/decline by:). model_prices — OVERRIDE map: model id -> out: cost-units per 1k tokens, 1e6 units = $1 (a $3/M-token rate is 3000). Beats the RubyLLM registry, which prices everything else per (model, provider).



94
95
96
# File 'lib/silas/configuration.rb', line 94

def inbox_actor
  @inbox_actor
end

#inbox_authObject

Inbox (mountable UI at /silas/inbox). inbox_auth — deny-by-default lambda; the host renders/head-404s to DENY and passes by NOT rendering (resilience pattern). inbox_public_read — reads render for anyone; approve/decline still gated. inbox_actor — controller -> identity string (approved_by/decline by:). model_prices — OVERRIDE map: model id -> out: cost-units per 1k tokens, 1e6 units = $1 (a $3/M-token rate is 3000). Beats the RubyLLM registry, which prices everything else per (model, provider).



94
95
96
# File 'lib/silas/configuration.rb', line 94

def inbox_auth
  @inbox_auth
end

#inbox_public_readObject

Inbox (mountable UI at /silas/inbox). inbox_auth — deny-by-default lambda; the host renders/head-404s to DENY and passes by NOT rendering (resilience pattern). inbox_public_read — reads render for anyone; approve/decline still gated. inbox_actor — controller -> identity string (approved_by/decline by:). model_prices — OVERRIDE map: model id -> out: cost-units per 1k tokens, 1e6 units = $1 (a $3/M-token rate is 3000). Beats the RubyLLM registry, which prices everything else per (model, provider).



94
95
96
# File 'lib/silas/configuration.rb', line 94

def inbox_public_read
  @inbox_public_read
end

#inbox_streamingObject

Force-disable live broadcasting even when turbo-rails is present (nil = auto).



96
97
98
# File 'lib/silas/configuration.rb', line 96

def inbox_streaming
  @inbox_streaming
end

#instructions_dirObject

Subagents: the roster (name+description) and per-name scope builders, plus the active-agent overrides swapped in during a nested run.



36
37
38
# File 'lib/silas/configuration.rb', line 36

def instructions_dir
  @instructions_dir
end

#isolate_stepsObject

Continuation isolation for loop steps. true in production (persistence per step — the durability contract); specs may disable for inline runs.



29
30
31
# File 'lib/silas/configuration.rb', line 29

def isolate_steps
  @isolate_steps
end

#max_stepsObject

Hard cap on model calls per turn (agent.yml can lower it per-agent).



18
19
20
# File 'lib/silas/configuration.rb', line 18

def max_steps
  @max_steps
end

#mcp_client_factoryObject

Connections: a test seam for injecting a fake MCP client ->(connection){ client }.



100
101
102
# File 'lib/silas/configuration.rb', line 100

def mcp_client_factory
  @mcp_client_factory
end

#mcp_server_hostObject

Bind host for the in-process MCP server (Mcp::Server — the "mount your tools as MCP" seam).



56
57
58
# File 'lib/silas/configuration.rb', line 56

def mcp_server_host
  @mcp_server_host
end

#memoryObject

Memory (silas_memories): memory=false disables entirely; memory_approval :always parks every remember for a human (default), :never auto-approves.



43
44
45
# File 'lib/silas/configuration.rb', line 43

def memory
  @memory
end

#memory_approvalObject

Memory (silas_memories): memory=false disables entirely; memory_approval :always parks every remember for a human (default), :never auto-approves.



43
44
45
# File 'lib/silas/configuration.rb', line 43

def memory_approval
  @memory_approval
end

#memory_injection_limitObject

Memory (silas_memories): memory=false disables entirely; memory_approval :always parks every remember for a human (default), :never auto-approves.



43
44
45
# File 'lib/silas/configuration.rb', line 43

def memory_injection_limit
  @memory_injection_limit
end

#model_pricesObject

Inbox (mountable UI at /silas/inbox). inbox_auth — deny-by-default lambda; the host renders/head-404s to DENY and passes by NOT rendering (resilience pattern). inbox_public_read — reads render for anyone; approve/decline still gated. inbox_actor — controller -> identity string (approved_by/decline by:). model_prices — OVERRIDE map: model id -> out: cost-units per 1k tokens, 1e6 units = $1 (a $3/M-token rate is 3000). Beats the RubyLLM registry, which prices everything else per (model, provider).



94
95
96
# File 'lib/silas/configuration.rb', line 94

def model_prices
  @model_prices
end

#named_agent_scopesObject

Named top-level agents (app/agents//): lambda -> { name => AgentScope }.



39
40
41
# File 'lib/silas/configuration.rb', line 39

def named_agent_scopes
  @named_agent_scopes
end

#queue_nameObject

Active Job queue for agent turns.



10
11
12
# File 'lib/silas/configuration.rb', line 10

def queue_name
  @queue_name
end

#sandboxObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



103
104
105
# File 'lib/silas/configuration.rb', line 103

def sandbox
  @sandbox
end

#sandbox_cpusObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



103
104
105
# File 'lib/silas/configuration.rb', line 103

def sandbox_cpus
  @sandbox_cpus
end

#sandbox_docker_binObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



103
104
105
# File 'lib/silas/configuration.rb', line 103

def sandbox_docker_bin
  @sandbox_docker_bin
end

#sandbox_imageObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



103
104
105
# File 'lib/silas/configuration.rb', line 103

def sandbox_image
  @sandbox_image
end

#sandbox_memoryObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



103
104
105
# File 'lib/silas/configuration.rb', line 103

def sandbox_memory
  @sandbox_memory
end

#sandbox_networkObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



103
104
105
# File 'lib/silas/configuration.rb', line 103

def sandbox_network
  @sandbox_network
end

#sandbox_pidsObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



103
104
105
# File 'lib/silas/configuration.rb', line 103

def sandbox_pids
  @sandbox_pids
end

#sandbox_timeoutObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



103
104
105
# File 'lib/silas/configuration.rb', line 103

def sandbox_timeout
  @sandbox_timeout
end

#sandbox_workdirObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



103
104
105
# File 'lib/silas/configuration.rb', line 103

def sandbox_workdir
  @sandbox_workdir
end

#schedulesObject

Injectable seams until/beyond the Registry: name -> tool object, the tool schema list for the model, and the definitions digest for the nondeterminism guard. The Registry wires real defaults at boot.



33
34
35
# File 'lib/silas/configuration.rb', line 33

def schedules
  @schedules
end

#skillsObject

Injectable seams until/beyond the Registry: name -> tool object, the tool schema list for the model, and the definitions digest for the nondeterminism guard. The Registry wires real defaults at boot.



33
34
35
# File 'lib/silas/configuration.rb', line 33

def skills
  @skills
end

#slack_bot_tokenObject



110
111
112
# File 'lib/silas/configuration.rb', line 110

def slack_bot_token
  @slack_bot_token || Rails.application.credentials.dig(:silas, :slack, :bot_token)
end

#slack_signing_secretObject



106
107
108
# File 'lib/silas/configuration.rb', line 106

def slack_signing_secret
  @slack_signing_secret || Rails.application.credentials.dig(:silas, :slack, :signing_secret)
end

#subagent_indexObject

Subagents: the roster (name+description) and per-name scope builders, plus the active-agent overrides swapped in during a nested run.



36
37
38
# File 'lib/silas/configuration.rb', line 36

def subagent_index
  @subagent_index
end

#subagent_scopesObject

Subagents: the roster (name+description) and per-name scope builders, plus the active-agent overrides swapped in during a nested run.



36
37
38
# File 'lib/silas/configuration.rb', line 36

def subagent_scopes
  @subagent_scopes
end

#tool_definitionsObject

Injectable seams until/beyond the Registry: name -> tool object, the tool schema list for the model, and the definitions digest for the nondeterminism guard. The Registry wires real defaults at boot.



33
34
35
# File 'lib/silas/configuration.rb', line 33

def tool_definitions
  @tool_definitions
end

#tool_resolverObject

Injectable seams until/beyond the Registry: name -> tool object, the tool schema list for the model, and the definitions digest for the nondeterminism guard. The Registry wires real defaults at boot.



33
34
35
# File 'lib/silas/configuration.rb', line 33

def tool_resolver
  @tool_resolver
end

Instance Method Details

#boot_guard!Object

Fail-loud misconfiguration checks, run from Silas.configure and at boot.



175
176
177
178
179
180
181
182
183
184
185
# File 'lib/silas/configuration.rb', line 175

def boot_guard!
  if adapter == :agent_sdk
    raise BootGuardError,
          "the :agent_sdk adapter was removed in Silas 0.2 — the claude -p subprocess " \
          "integration is gone (its subscription-auth rationale was unreachable). " \
          "Use adapter :ruby_llm, the production path."
  end

  check_provider_credentials!
  warn_unsafe_queue_adapter!
end

#check_provider_credentials!Object

The most common first-run failure: no provider key configured, so the first turn dies deep inside RubyLLM with a third-party error. Surface it at boot with the fix. Raises in production (a keyless prod deploy is always a misconfiguration); warns in development so a fresh app can still boot and browse the inbox before a key exists.



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/silas/configuration.rb', line 192

def check_provider_credentials!
  return unless adapter == :ruby_llm && defined?(::RubyLLM)

  providers = ::RubyLLM::Provider.providers.values
  configured = providers.any? do |provider|
    requirements = provider.configuration_requirements
    requirements.any? && requirements.all? { |key| ::RubyLLM.config.public_send(key).present? }
  end
  return if configured

  message = "[Silas] adapter :ruby_llm has no configured provider — no API key is set on " \
            "RubyLLM.config. Set one in config/initializers/ruby_llm.rb, e.g. " \
            "RubyLLM.configure { |c| c.anthropic_api_key = ENV[\"ANTHROPIC_API_KEY\"] } " \
            "— the first agent turn will fail without it."
  raise BootGuardError, message if defined?(::Rails) && ::Rails.env.production?

  (defined?(::Rails) && ::Rails.logger ? ::Rails.logger.warn(message) : nil) || Kernel.warn(message)
rescue BootGuardError
  raise
rescue StandardError
  nil # a diagnostic must never break boot
end

#engineObject

Renamed in 0.4, removed in 2.0. "engine" meant two unrelated things — the Rails engine (Silas::Engine) and the inference backend — the exact collision ActiveJob avoids by calling its seam QueueAdapters.



61
62
63
64
# File 'lib/silas/configuration.rb', line 61

def engine
  Silas.deprecator.warn("config.engine is deprecated; use config.adapter")
  adapter
end

#engine=(value) ⇒ Object



66
67
68
69
# File 'lib/silas/configuration.rb', line 66

def engine=(value)
  Silas.deprecator.warn("config.engine= is deprecated; use config.adapter=")
  self.adapter = value
end

#validate!Object



169
170
171
172
# File 'lib/silas/configuration.rb', line 169

def validate!
  boot_guard!
  self
end

#warn_unsafe_queue_adapter!Object

Silas's durability and exactly-once guarantees rest on the queue adapter: a turn's jobs must run one-at-a-time and continuation retries must run in a FRESH execution after the prior one committed. The in-process Async adapter (ActiveJob's dev default) breaks both — it runs a re-enqueued continuation on a thread pool concurrently with the original, which double-executes steps and violates the single-active-turn invariant (a re-run model call mints new tool_call ids the ledger cannot dedup). Solid Queue — or any durable, serializing, DB-backed adapter — is required to run agents; the synchronous :inline adapter is safe for scripts and demos (no durability).

RAISES in production — running agents on the Async adapter there silently voids the durability contract. Warns in development (Rails' dev default is :async and a fresh app must still boot).



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/silas/configuration.rb', line 228

def warn_unsafe_queue_adapter!
  return unless defined?(::ActiveJob::Base)

  name = ::ActiveJob::Base.queue_adapter.class.name.to_s
  return unless name.include?("AsyncAdapter")

  message = "[Silas] ActiveJob is using the in-process Async queue adapter. " \
            "This runs continuation retries on a thread pool concurrently with " \
            "the original job, which double-executes agent steps and breaks " \
            "exactly-once tool effects. Use :solid_queue (production) or " \
            ":inline (scripts/demos). See Silas DEPLOY.md."
  raise BootGuardError, message if defined?(::Rails) && ::Rails.env.production?

  (defined?(::Rails) && ::Rails.logger ? ::Rails.logger.warn(message) : nil) || Kernel.warn(message)
rescue BootGuardError
  raise
rescue StandardError
  # A diagnostic must never break boot.
  nil
end