silas 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +238 -0
- data/README.md +11 -5
- data/app/controllers/silas/channels/slack_controller.rb +2 -0
- data/app/controllers/silas/inbox/sessions_controller.rb +3 -1
- data/app/helpers/silas/inbox/trace_helper.rb +224 -2
- data/app/mailboxes/silas/agent_mailbox.rb +4 -0
- data/app/models/silas/tool_invocation.rb +37 -6
- data/app/views/layouts/silas/inbox.html.erb +69 -3
- data/app/views/silas/inbox/invocations/_detail.html.erb +21 -0
- data/app/views/silas/inbox/invocations/_invocation.html.erb +39 -24
- data/app/views/silas/inbox/sessions/_child.html.erb +14 -0
- data/app/views/silas/inbox/sessions/_row.html.erb +13 -2
- data/app/views/silas/inbox/sessions/show.html.erb +33 -0
- data/app/views/silas/inbox/steps/_step.html.erb +8 -1
- data/app/views/silas/inbox/turns/_header.html.erb +7 -2
- data/docs/agents.md +15 -3
- data/docs/channels.md +53 -2
- data/docs/configuration.md +6 -1
- data/docs/connections.md +8 -5
- data/docs/guarantees.md +16 -5
- data/docs/headless.md +120 -0
- data/docs/providers.md +171 -0
- data/docs/traces.md +131 -0
- data/docs/tutorial.md +6 -4
- data/docs/vs-eve.md +41 -17
- data/docs/why-silas.md +6 -6
- data/lib/generators/silas/install/install_generator.rb +21 -0
- data/lib/generators/silas/install/templates/initializer.rb +5 -0
- data/lib/generators/silas/install/templates/ruby_llm.rb +3 -1
- data/lib/silas/channel.rb +85 -11
- data/lib/silas/configuration.rb +16 -1
- data/lib/silas/doctor.rb +35 -2
- data/lib/silas/registry.rb +20 -2
- data/lib/silas/tool.rb +38 -5
- data/lib/silas/version.rb +1 -1
- metadata +6 -1
data/lib/silas/configuration.rb
CHANGED
|
@@ -50,9 +50,23 @@ module Silas
|
|
|
50
50
|
# Channels: name -> Channel subclass (wired by the Registry). Slack creds
|
|
51
51
|
# default to credentials.dig(:silas, :slack, ...); nil disables Slack.
|
|
52
52
|
attr_accessor :channel_resolver
|
|
53
|
+
# Inbound routing: which named agent an external thread wakes. Data only —
|
|
54
|
+
# { transport => { key => agent_name } }, where transport is the channel's
|
|
55
|
+
# filename identity and key is whatever that transport calls a destination:
|
|
56
|
+
#
|
|
57
|
+
# config.channel_routes = {
|
|
58
|
+
# "slack" => { "C0BILLING" => "bookkeeper" },
|
|
59
|
+
# "email" => { "billing@shop.test" => "bookkeeper" }
|
|
60
|
+
# }
|
|
61
|
+
#
|
|
62
|
+
# Unmatched threads wake the root agent. Names are checked at boot against
|
|
63
|
+
# app/agents/ (Registry.install!) — a typo fails the deploy, never a
|
|
64
|
+
# webhook.
|
|
65
|
+
attr_accessor :channel_routes
|
|
53
66
|
attr_writer :slack_signing_secret, :slack_bot_token
|
|
54
67
|
# Bind host for the in-process MCP server (Mcp::Server — the "mount your
|
|
55
|
-
# tools as MCP" seam).
|
|
68
|
+
# tools as MCP" seam). Inert: nothing outside Mcp::Server's own specs calls
|
|
69
|
+
# .start, so this setting has no effect until a mounted endpoint ships.
|
|
56
70
|
attr_accessor :mcp_server_host
|
|
57
71
|
|
|
58
72
|
# Renamed in 0.4, removed in 2.0. "engine" meant two unrelated things —
|
|
@@ -134,6 +148,7 @@ module Silas
|
|
|
134
148
|
@agent_override = nil
|
|
135
149
|
@instructions_dir = nil
|
|
136
150
|
@channel_resolver = nil
|
|
151
|
+
@channel_routes = {}
|
|
137
152
|
@slack_signing_secret = nil
|
|
138
153
|
@slack_bot_token = nil
|
|
139
154
|
@mcp_server_host = "127.0.0.1"
|
data/lib/silas/doctor.rb
CHANGED
|
@@ -10,6 +10,35 @@ module Silas
|
|
|
10
10
|
class Doctor
|
|
11
11
|
Check = Struct.new(:status, :label, :detail) # status: :pass | :warn | :fail
|
|
12
12
|
|
|
13
|
+
# Rails defaults development to :async, which the queue_adapter check below
|
|
14
|
+
# FAILS — so a stock app fails the doctor the installer told it to run. The
|
|
15
|
+
# remedy travels with the failure, and the install generator prints this
|
|
16
|
+
# same constant when it detects :async, so the two surfaces can't drift.
|
|
17
|
+
# Printed, never written: database.yml and cable.yml belong to the host app.
|
|
18
|
+
ASYNC_QUEUE_REMEDY = <<~MSG.freeze
|
|
19
|
+
Add to config/environments/development.rb:
|
|
20
|
+
|
|
21
|
+
config.active_job.queue_adapter = :solid_queue
|
|
22
|
+
config.solid_queue.connects_to = { database: { writing: :queue } }
|
|
23
|
+
|
|
24
|
+
and a queue database to config/database.yml (SQLite shown — drop the
|
|
25
|
+
connects_to line above if Solid Queue shares your primary database):
|
|
26
|
+
|
|
27
|
+
development:
|
|
28
|
+
primary:
|
|
29
|
+
<<: *default
|
|
30
|
+
database: storage/development.sqlite3
|
|
31
|
+
queue:
|
|
32
|
+
<<: *default
|
|
33
|
+
database: storage/development_queue.sqlite3
|
|
34
|
+
migrations_paths: db/queue_migrate
|
|
35
|
+
|
|
36
|
+
Then `bin/rails db:prepare`, and run the worker next to the server:
|
|
37
|
+
`bin/jobs`. On an app that predates Rails 8, `bundle add solid_queue &&
|
|
38
|
+
bin/rails solid_queue:install` first. For scripts and demos `:inline` is
|
|
39
|
+
also safe — synchronous, no durability.
|
|
40
|
+
MSG
|
|
41
|
+
|
|
13
42
|
def self.run(root: Rails.root) = new(root: root).run
|
|
14
43
|
|
|
15
44
|
def initialize(root:)
|
|
@@ -49,11 +78,15 @@ module Silas
|
|
|
49
78
|
when /AsyncAdapter/
|
|
50
79
|
Check.new(:fail, "queue adapter",
|
|
51
80
|
"in-process :async double-executes continuation steps and voids the durability " \
|
|
52
|
-
"contract — use :solid_queue (see DEPLOY.md)")
|
|
81
|
+
"contract — use :solid_queue (see DEPLOY.md)\n\n#{ASYNC_QUEUE_REMEDY.indent(3).chomp}")
|
|
53
82
|
when /InlineAdapter/
|
|
54
83
|
Check.new(:warn, "queue adapter", "inline — fine for scripts and demos, no durability")
|
|
55
84
|
else
|
|
56
|
-
Check.new(:warn, "queue adapter",
|
|
85
|
+
Check.new(:warn, "queue adapter",
|
|
86
|
+
"#{name.demodulize} — no dead-job rescue path: DeadJobRescuerJob sweeps expired approvals " \
|
|
87
|
+
"but returns early unless SolidQueue is defined, so a job killed with its worker is never " \
|
|
88
|
+
"retried and a turn stranded mid-tool stays running with its in-doubt invocation unswept. " \
|
|
89
|
+
"Durability requires a serializing, DB-backed adapter.")
|
|
57
90
|
end
|
|
58
91
|
end
|
|
59
92
|
|
data/lib/silas/registry.rb
CHANGED
|
@@ -20,6 +20,10 @@ module Silas
|
|
|
20
20
|
Silas.config.agent_override = nil
|
|
21
21
|
Silas.config.instructions_dir = nil
|
|
22
22
|
Silas.reset_agent_memo!
|
|
23
|
+
# Boot is the last place a bad channel route can fail safely. Directory
|
|
24
|
+
# names, not built scopes: validation must not force every named agent's
|
|
25
|
+
# tools to constantize before anything needs them.
|
|
26
|
+
Silas::Channel.validate_routes!(registry.named_agent_dirs.map { |dir| File.basename(dir) })
|
|
23
27
|
registry
|
|
24
28
|
end
|
|
25
29
|
|
|
@@ -182,6 +186,13 @@ module Silas
|
|
|
182
186
|
# identity under const_base, skills, the load_skill builtin when skills
|
|
183
187
|
# exist, run_code when asked, and the scope's own digest (the same
|
|
184
188
|
# NondeterminismError guard root turns get).
|
|
189
|
+
#
|
|
190
|
+
# A named agent is a full member of staff: it gets ask_question (parking to
|
|
191
|
+
# ask a person is the premise, not a root-agent privilege) and the shared
|
|
192
|
+
# remote connections. Connections live in the root app/agent/connections/ —
|
|
193
|
+
# one set of credentials for the app, reachable by everyone. Subagents get
|
|
194
|
+
# neither: they run inside a parent's turn, which is where the human contact
|
|
195
|
+
# and the remote surface belong.
|
|
185
196
|
def build_agent_scope(dir, name, const_base:, agent: nil, run_code: false, named: false)
|
|
186
197
|
tools = Dir[dir.join("tools/*.rb")].sort.to_h do |file|
|
|
187
198
|
tname = File.basename(file, ".rb")
|
|
@@ -193,6 +204,7 @@ module Silas
|
|
|
193
204
|
end
|
|
194
205
|
skills = Dir[dir.join("skills/*.md")].sort.map { |f| Skill.parse(f) }
|
|
195
206
|
builtins = {}
|
|
207
|
+
builtins["ask_question"] = Silas::Tools::AskQuestion if named && Silas.config.ask_question
|
|
196
208
|
builtins["load_skill"] = Silas::Tools::LoadSkill if skills.any?
|
|
197
209
|
builtins["run_code"] = Silas::Tools::RunCode if run_code
|
|
198
210
|
if named && Silas.memory_enabled?
|
|
@@ -200,8 +212,14 @@ module Silas
|
|
|
200
212
|
builtins["recall"] = Silas::Tools::Recall
|
|
201
213
|
end
|
|
202
214
|
builtins["handoff"] = Silas::Tools::Handoff if named && named_agent_dirs.size > 1
|
|
203
|
-
|
|
204
|
-
|
|
215
|
+
remote = connections if named
|
|
216
|
+
resolver = lambda do |n|
|
|
217
|
+
klass = tools[n] || builtins[n]
|
|
218
|
+
return klass.new if klass
|
|
219
|
+
|
|
220
|
+
remote&.resolve(n) or raise Error, "unknown tool #{n.inspect} for agent #{name.inspect}"
|
|
221
|
+
end
|
|
222
|
+
definitions = (tools.values + builtins.values).map(&:schema) + (remote&.definitions || [])
|
|
205
223
|
loaded_agent = agent || Silas::Agent.load(dir: dir)
|
|
206
224
|
payload = { tools: definitions, skills: skills.map { |s| [ s.name, s.description ] } }
|
|
207
225
|
payload[:final_answer] = loaded_agent.final_answer if loaded_agent.final_answer.present?
|
data/lib/silas/tool.rb
CHANGED
|
@@ -23,16 +23,16 @@ module Silas
|
|
|
23
23
|
class << self
|
|
24
24
|
def description(text = nil)
|
|
25
25
|
@description = text if text
|
|
26
|
-
|
|
26
|
+
inherited_setting(:@description) || ""
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def param(name, type = :string, desc: nil)
|
|
30
|
-
|
|
30
|
+
own_param_refinements[name.to_sym] = { type: type.to_s, desc: desc }
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def approval(policy = nil, &block)
|
|
34
34
|
@approval_policy = block || policy unless policy.nil? && block.nil?
|
|
35
|
-
|
|
35
|
+
inherited_setting(:@approval_policy) || :never
|
|
36
36
|
end
|
|
37
37
|
alias approval_policy approval
|
|
38
38
|
|
|
@@ -40,7 +40,7 @@ module Silas
|
|
|
40
40
|
def idempotent! = @effect_mode = :idempotent
|
|
41
41
|
def at_most_once! = @effect_mode = :at_most_once
|
|
42
42
|
|
|
43
|
-
def effect_mode =
|
|
43
|
+
def effect_mode = inherited_setting(:@effect_mode) || :at_most_once
|
|
44
44
|
|
|
45
45
|
def tool_name
|
|
46
46
|
name.demodulize.underscore
|
|
@@ -79,9 +79,42 @@ module Silas
|
|
|
79
79
|
|
|
80
80
|
private
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
# Ruby does NOT inherit class-level instance variables. Without this walk,
|
|
83
|
+
# factoring shared declarations into a base class —
|
|
84
|
+
#
|
|
85
|
+
# class MoneyTool < Silas::Tool
|
|
86
|
+
# transactional!
|
|
87
|
+
# approval :always
|
|
88
|
+
# end
|
|
89
|
+
# class IssueRefund < MoneyTool; end
|
|
90
|
+
#
|
|
91
|
+
# — silently drops both in the subclass, and drops them into the LEAST
|
|
92
|
+
# safe defaults: no database transaction (:at_most_once) and no human
|
|
93
|
+
# gate (:never). Silently disarming the two declarations the ledger acts
|
|
94
|
+
# on is the worst failure this DSL could have, so settings resolve up the
|
|
95
|
+
# ancestry: nearest explicit declaration wins.
|
|
96
|
+
def inherited_setting(ivar)
|
|
97
|
+
klass = self
|
|
98
|
+
while klass.respond_to?(:inherited_setting, true)
|
|
99
|
+
value = klass.instance_variable_get(ivar)
|
|
100
|
+
return value unless value.nil?
|
|
101
|
+
|
|
102
|
+
klass = klass.superclass
|
|
103
|
+
end
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def own_param_refinements
|
|
83
108
|
@param_refinements ||= {}
|
|
84
109
|
end
|
|
110
|
+
|
|
111
|
+
# Merged down the ancestry so a base class's `param` refinements survive;
|
|
112
|
+
# a subclass redeclaring the same param wins.
|
|
113
|
+
def param_refinements
|
|
114
|
+
return own_param_refinements unless superclass.respond_to?(:param_refinements, true)
|
|
115
|
+
|
|
116
|
+
superclass.send(:param_refinements).merge(own_param_refinements)
|
|
117
|
+
end
|
|
85
118
|
end
|
|
86
119
|
|
|
87
120
|
# Instance-side delegation so a resolved instance answers the Ledger.
|
data/lib/silas/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: silas
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel St Paul
|
|
@@ -146,7 +146,9 @@ files:
|
|
|
146
146
|
- app/views/silas/channels/approvals/error.html.erb
|
|
147
147
|
- app/views/silas/channels/approvals/show.html.erb
|
|
148
148
|
- app/views/silas/inbox/invocations/_approval_card.html.erb
|
|
149
|
+
- app/views/silas/inbox/invocations/_detail.html.erb
|
|
149
150
|
- app/views/silas/inbox/invocations/_invocation.html.erb
|
|
151
|
+
- app/views/silas/inbox/sessions/_child.html.erb
|
|
150
152
|
- app/views/silas/inbox/sessions/_cost.html.erb
|
|
151
153
|
- app/views/silas/inbox/sessions/_row.html.erb
|
|
152
154
|
- app/views/silas/inbox/sessions/index.html.erb
|
|
@@ -175,10 +177,13 @@ files:
|
|
|
175
177
|
- docs/conventions.md
|
|
176
178
|
- docs/evals.md
|
|
177
179
|
- docs/guarantees.md
|
|
180
|
+
- docs/headless.md
|
|
178
181
|
- docs/inbox-and-api.md
|
|
179
182
|
- docs/memory.md
|
|
183
|
+
- docs/providers.md
|
|
180
184
|
- docs/sandbox.md
|
|
181
185
|
- docs/tools.md
|
|
186
|
+
- docs/traces.md
|
|
182
187
|
- docs/tutorial.md
|
|
183
188
|
- docs/vs-eve.md
|
|
184
189
|
- docs/why-silas.md
|