Module: Silas

Defined in:
lib/silas.rb,
lib/silas/chat.rb,
lib/silas/eval.rb,
lib/silas/tool.rb,
lib/silas/agent.rb,
lib/silas/inbox.rb,
lib/silas/skill.rb,
lib/silas/slack.rb,
lib/silas/budget.rb,
lib/silas/doctor.rb,
lib/silas/engine.rb,
lib/silas/errors.rb,
lib/silas/ledger.rb,
lib/silas/channel.rb,
lib/silas/sandbox.rb,
lib/silas/version.rb,
lib/silas/webhook.rb,
lib/silas/eval/dsl.rb,
lib/silas/registry.rb,
lib/silas/schedule.rb,
lib/silas/compactor.rb,
lib/silas/connection.rb,
lib/silas/deprecator.rb,
lib/silas/inbox/cost.rb,
lib/silas/mcp/client.rb,
lib/silas/mcp/server.rb,
app/models/silas/step.rb,
app/models/silas/turn.rb,
lib/silas/agent_scope.rb,
lib/silas/connections.rb,
lib/silas/eval/driver.rb,
lib/silas/eval/grader.rb,
lib/silas/eval/result.rb,
lib/silas/eval/runner.rb,
lib/silas/mcp/handler.rb,
lib/silas/named_agent.rb,
lib/silas/step_runner.rb,
lib/silas/delta_buffer.rb,
lib/silas/instructions.rb,
lib/silas/sandbox/null.rb,
lib/silas/tools/recall.rb,
app/models/silas/memory.rb,
lib/silas/adapters/base.rb,
lib/silas/configuration.rb,
lib/silas/nested_runner.rb,
lib/silas/tools/handoff.rb,
app/models/silas/session.rb,
lib/silas/sandbox/docker.rb,
lib/silas/tools/delegate.rb,
lib/silas/tools/remember.rb,
lib/silas/tools/run_code.rb,
lib/silas/eval/assertions.rb,
lib/silas/eval/transcript.rb,
lib/silas/instrumentation.rb,
lib/silas/message_builder.rb,
lib/silas/tools/load_skill.rb,
app/jobs/silas/schedule_job.rb,
app/models/silas/compaction.rb,
lib/silas/adapters/ruby_llm.rb,
lib/silas/schedule/compiler.rb,
lib/silas/tools/ask_question.rb,
app/jobs/silas/agent_loop_job.rb,
lib/silas/eval/scripted_engine.rb,
app/mailers/silas/channel_mailer.rb,
app/models/silas/tool_invocation.rb,
app/mailboxes/silas/agent_mailbox.rb,
lib/silas/inbox/delta_broadcaster.rb,
app/jobs/silas/channel_delivery_job.rb,
app/jobs/silas/dead_job_rescuer_job.rb,
app/models/silas/application_record.rb,
app/helpers/silas/inbox/trace_helper.rb,
app/controllers/silas/api/base_controller.rb,
app/controllers/silas/inbox/base_controller.rb,
app/controllers/silas/inbox/turns_controller.rb,
app/controllers/silas/api/v1/turns_controller.rb,
app/models/concerns/silas/inbox/broadcastable.rb,
app/controllers/silas/channels/base_controller.rb,
lib/generators/silas/channel/channel_generator.rb,
lib/generators/silas/install/install_generator.rb,
app/controllers/silas/api/v1/streams_controller.rb,
app/controllers/silas/channels/slack_controller.rb,
app/controllers/silas/inbox/sessions_controller.rb,
app/controllers/concerns/silas/api/serialization.rb,
app/controllers/silas/api/v1/sessions_controller.rb,
app/controllers/silas/api/v1/approvals_controller.rb,
app/controllers/silas/inbox/invocations_controller.rb,
app/controllers/silas/channels/approvals_controller.rb

Defined Under Namespace

Modules: Adapters, Api, Budget, Channels, Compactor, Engines, Eval, Generators, Inbox, Instructions, Instrumentation, Ledger, Mcp, MessageBuilder, NestedRunner, Sandbox, Slack, StepRunner, Tools, Webhook Classes: Agent, AgentLoopJob, AgentMailbox, AgentScope, ApplicationRecord, BootGuardError, Channel, ChannelDeliveryJob, ChannelMailer, Chat, CheckpointInLedgerError, Compaction, Configuration, Connection, Connections, DeadJobRescuerJob, DeltaBuffer, Doctor, Engine, Error, Event, LogSubscriber, Memory, NamedAgent, NondeterminismError, Registry, SandboxDisabledError, SandboxError, Schedule, ScheduleJob, Session, Skill, Step, Tool, ToolInvocation, Turn, TurnInProgressError

Constant Summary collapse

SCOPE_KEY =

---- scope switching -----------------------------------------------------

:silas_agent_scope
VERSION =
"0.5.0"

Class Method Summary collapse

Class Method Details

.agent(name = nil) ⇒ Object

The active agent definition — or, given a name, a handle for a NAMED agent (app/agents//) whose sessions run under that agent's scope:

Silas.agent.start(input: "...")            # the root app/agent
Silas.agent(:clerk).start(input: "...")    # a named staff member


173
174
175
176
177
# File 'lib/silas.rb', line 173

def agent(name = nil)
  return NamedAgent.new(named_agent_scope!(name)) if name

  current_scope&.agent || config.agent_override || (@agent ||= Agent.load)
end

.configObject



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

def config
  @config ||= Configuration.new
end

.configure {|config| ... } ⇒ Object

Reconfiguring re-resolves the engine and sandbox. Without this the memos below outlive the config that produced them: a second Silas.configure { |c| c.engine = ... } in one process kept serving the FIRST engine. That silently broke multi-scenario silas:eval runs — every scenario after the first ran the first one's script and still reported pass/fail as if it hadn't. Re-resolution is cheap (one .new).

Yields:



66
67
68
69
70
71
72
# File 'lib/silas.rb', line 66

def configure
  yield config
  config.validate!
  @resolved_adapter = nil
  @resolved_sandbox = nil
  config
end

.current_scopeObject



222
223
224
# File 'lib/silas.rb', line 222

def current_scope
  ActiveSupport::IsolatedExecutionState[SCOPE_KEY]
end

.definitions_digestObject

The live definitions digest as a String (nil when none configured).



160
161
162
# File 'lib/silas.rb', line 160

def definitions_digest
  current_scope&.digest || config.definitions_digest&.call&.to_s.presence
end

.deprecatorObject

One deprecator for the whole gem, so hosts can control the noise the way they control Rails' own:

Silas.deprecator.behavior = :raise    # or :warn (default), :silence

Rails registers it in application.deprecators (see Silas::Engine), which means config.active_support.report_deprecations = false silences Silas along with everything else, and a host can opt into raising in CI.

Every deprecation message names BOTH the replacement and the version it disappears in — a warning you can't act on is just noise.



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

def self.deprecator
  @deprecator ||= ActiveSupport::Deprecation.new("2.0", "Silas")
end

.instructions_dirObject



164
165
166
# File 'lib/silas.rb', line 164

def instructions_dir
  current_scope&.dir || config.instructions_dir
end

.instrument(event, **payload, &block) ⇒ Object

Silas.instrument(:tool, tool: "issue_refund") { ... }



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

def self.instrument(event, **payload, &block)
  Instrumentation.instrument(event, **payload, &block)
end

.loggerObject



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

def self.logger
  @@logger ||= (defined?(Rails) && Rails.logger) || ActiveSupport::Logger.new($stdout)
end

.memory_enabled?Boolean

Memory is on when configured AND the table exists (upgrade-safe: an app that hasn't run the 0.1.7 migration simply doesn't advertise the tools).

Returns:

  • (Boolean)


184
185
186
187
188
# File 'lib/silas.rb', line 184

def memory_enabled?
  config.memory && Memory.table_exists?
rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
  false
end

.named_agent?(name) ⇒ Boolean

Returns:

  • (Boolean)


189
# File 'lib/silas.rb', line 189

def named_agent?(name) = named_agent_scopes.key?(name.to_s)

.named_agent_scope!(name) ⇒ Object



191
192
193
194
195
196
197
# File 'lib/silas.rb', line 191

def named_agent_scope!(name)
  named_agent_scopes.fetch(name.to_s) do
    known = named_agent_scopes.keys
    raise Error, "unknown agent #{name.inspect}" \
                 "#{known.any? ? " (known: #{known.join(', ')})" : " — no app/agents/ directories found"}"
  end
end

.named_agent_scopesObject

Named-agent roster: { "name" => AgentScope }.



180
# File 'lib/silas.rb', line 180

def named_agent_scopes = config.named_agent_scopes&.call || {}

.reset_agent_memo!Object

after Registry.install! swaps dirs



110
# File 'lib/silas.rb', line 110

def reset_agent_memo! = (@agent = nil) # after Registry.install! swaps dirs

.reset_configuration!Object

for specs



74
75
76
77
78
79
# File 'lib/silas.rb', line 74

def reset_configuration! # for specs
  @config = nil
  @resolved_adapter = nil
  @resolved_sandbox = nil
  @agent = nil
end

.resolved_adapterObject

The inference adapter instance. config.adapter may be :ruby_llm or any object responding to #execute_step (specs, custom).



114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/silas.rb', line 114

def resolved_adapter
  @resolved_adapter ||=
    case config.adapter
    when :ruby_llm then Adapters::RubyLLM.new
    when :agent_sdk
      raise Error, "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."
    when Symbol then raise Error, "unknown adapter #{config.adapter.inspect}"
    else config.adapter
    end
end

.resolved_engineObject

Renamed in 0.4: "engine" meant two unrelated things (the Rails engine at Silas::Engine, and the inference backend), which is exactly the collision ActiveJob avoids with QueueAdapters. Removed in 2.0.



130
131
132
133
# File 'lib/silas.rb', line 130

def resolved_engine
  Silas.deprecator.warn("Silas.resolved_engine is deprecated; use Silas.resolved_adapter")
  resolved_adapter
end

.resolved_sandboxObject



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/silas.rb', line 89

def resolved_sandbox
  @resolved_sandbox ||=
    case config.sandbox
    when :none, nil then Sandbox::Null.new
    when :docker
      Sandbox::Docker.new(image: config.sandbox_image, network: config.sandbox_network,
                          memory: config.sandbox_memory, cpus: config.sandbox_cpus,
                          pids: config.sandbox_pids, workdir: config.sandbox_workdir,
                          docker_bin: config.sandbox_docker_bin)
    when Symbol then raise Error, "unknown sandbox #{config.sandbox.inspect}"
    else
      # A hermetic backend drops straight in (its Result is a superset of
      # ours). Auto-arm its ledger guard so a sandbox exec inside a ledger
      # transaction fails loud — same posture as our own Docker adapter.
      if defined?(Hermetic::Backends::Base) && config.sandbox.is_a?(Hermetic::Backends::Base)
        require "hermetic/silas"
      end
      config.sandbox
    end
end

.sandbox_enabled?Boolean

A configured-but-disabled backend (e.g. Hermetic.null) must not register run_code, so the object's own enabled? is the final word.

Returns:

  • (Boolean)


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

def sandbox_enabled?
  return false if [ :none, nil ].include?(config.sandbox)

  resolved_sandbox.enabled?
end

.schedulesObject



155
156
157
# File 'lib/silas.rb', line 155

def schedules
  config.schedules&.call || []
end

.scope_for_session(session) ⇒ Object

The scope a session's turns must run under: nil for the root agent, otherwise the named-agent or subagent scope matching session.agent_name. Fails loud on an unknown name — a session pointing at a deleted agent directory must never silently run with the root agent's tools.



209
210
211
212
213
214
215
216
# File 'lib/silas.rb', line 209

def scope_for_session(session)
  name = session.agent_name.to_s
  return nil if name.empty? || name == "agent"

  named_agent_scopes[name] || config.subagent_scopes&.call&.[](name) or
    raise Error, "session #{session.id} belongs to agent #{name.inspect}, " \
                 "but no app/agents/#{name} or app/agent/subagents/#{name} exists"
end

.skillsObject



151
152
153
# File 'lib/silas.rb', line 151

def skills
  current_scope&.skills || config.skills&.call || []
end

.subagent?(name) ⇒ Boolean

Returns:

  • (Boolean)


202
# File 'lib/silas.rb', line 202

def subagent?(name) = subagent_index.any? { |n, _| n == name.to_s }

.subagent_indexObject

Subagent roster: [[name, description], ...] (model-visible, so it's in the root digest via Delegate.description).



201
# File 'lib/silas.rb', line 201

def subagent_index = config.subagent_index&.call || []

.subagent_scope(name) ⇒ Object



203
# File 'lib/silas.rb', line 203

def subagent_scope(name) = config.subagent_scopes&.call&.fetch(name.to_s)

.tool_definitionsObject



147
148
149
# File 'lib/silas.rb', line 147

def tool_definitions
  current_scope&.definitions || config.tool_definitions&.call || []
end

.tool_resolverObject

---- scope-aware readers ------------------------------------------------- Every reader consults the active AgentScope first (named agent or subagent), falling back to the boot-time config the Registry installed. The scope lives in IsolatedExecutionState — per-thread AND per-fiber (Falcon-safe), so concurrent jobs running different agents in one process can never see each other's tools.



142
143
144
145
# File 'lib/silas.rb', line 142

def tool_resolver
  current_scope&.resolver ||
    config.tool_resolver or raise Error, "no tool resolver configured (Registry boots one; specs must inject)"
end

.with_agent_scope(scope) ⇒ Object

Run a block under an AgentScope. Nestable (delegation inside a named agent restores the outer scope on exit) and isolated per execution context — no global config is mutated, so concurrent jobs are safe.



229
230
231
232
233
234
235
# File 'lib/silas.rb', line 229

def with_agent_scope(scope)
  previous = ActiveSupport::IsolatedExecutionState[SCOPE_KEY]
  ActiveSupport::IsolatedExecutionState[SCOPE_KEY] = scope
  yield
ensure
  ActiveSupport::IsolatedExecutionState[SCOPE_KEY] = previous
end