silas 0.1.1
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 +7 -0
- data/CHANGELOG.md +41 -0
- data/LICENSE +21 -0
- data/README.md +135 -0
- data/app/controllers/silas/channels/approvals_controller.rb +34 -0
- data/app/controllers/silas/channels/base_controller.rb +22 -0
- data/app/controllers/silas/channels/slack_controller.rb +58 -0
- data/app/controllers/silas/inbox/base_controller.rb +37 -0
- data/app/controllers/silas/inbox/invocations_controller.rb +44 -0
- data/app/controllers/silas/inbox/sessions_controller.rb +16 -0
- data/app/helpers/silas/inbox/trace_helper.rb +42 -0
- data/app/jobs/silas/agent_loop_job.rb +95 -0
- data/app/jobs/silas/channel_delivery_job.rb +62 -0
- data/app/jobs/silas/dead_job_rescuer_job.rb +31 -0
- data/app/jobs/silas/schedule_job.rb +22 -0
- data/app/mailboxes/silas/agent_mailbox.rb +35 -0
- data/app/mailers/silas/channel_mailer.rb +17 -0
- data/app/models/concerns/silas/inbox/broadcastable.rb +86 -0
- data/app/models/silas/application_record.rb +5 -0
- data/app/models/silas/session.rb +36 -0
- data/app/models/silas/step.rb +33 -0
- data/app/models/silas/tool_invocation.rb +78 -0
- data/app/models/silas/turn.rb +58 -0
- data/app/views/layouts/silas/inbox.html.erb +91 -0
- data/app/views/silas/channel_mailer/approval.text.erb +9 -0
- data/app/views/silas/channels/approvals/done.html.erb +2 -0
- data/app/views/silas/channels/approvals/error.html.erb +2 -0
- data/app/views/silas/channels/approvals/show.html.erb +9 -0
- data/app/views/silas/inbox/invocations/_approval_card.html.erb +11 -0
- data/app/views/silas/inbox/invocations/_invocation.html.erb +11 -0
- data/app/views/silas/inbox/sessions/_cost.html.erb +1 -0
- data/app/views/silas/inbox/sessions/index.html.erb +23 -0
- data/app/views/silas/inbox/sessions/show.html.erb +20 -0
- data/app/views/silas/inbox/steps/_step.html.erb +8 -0
- data/app/views/silas/inbox/turns/_header.html.erb +7 -0
- data/app/views/silas/inbox/turns/_turn.html.erb +8 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20260714000001_create_silas_tables.rb +78 -0
- data/db/migrate/20260715000001_add_channel_outbound_markers.rb +8 -0
- data/db/migrate/20260715000002_add_agent_sdk_columns_to_turns.rb +9 -0
- data/db/migrate/20260715000003_add_parent_session_to_silas_sessions.rb +8 -0
- data/lib/generators/silas/install/install_generator.rb +81 -0
- data/lib/generators/silas/install/templates/agent.yml +9 -0
- data/lib/generators/silas/install/templates/bin_ci +7 -0
- data/lib/generators/silas/install/templates/channel_email.rb +18 -0
- data/lib/generators/silas/install/templates/channel_slack.rb +19 -0
- data/lib/generators/silas/install/templates/example_eval.rb +22 -0
- data/lib/generators/silas/install/templates/example_schedule.md +11 -0
- data/lib/generators/silas/install/templates/example_skill.md +8 -0
- data/lib/generators/silas/install/templates/example_tool.rb +12 -0
- data/lib/generators/silas/install/templates/initializer.rb +16 -0
- data/lib/generators/silas/install/templates/instructions.md +4 -0
- data/lib/silas/agent.rb +33 -0
- data/lib/silas/agent_scope.rb +6 -0
- data/lib/silas/agent_sdk/cli.rb +59 -0
- data/lib/silas/agent_sdk/stream_parser.rb +86 -0
- data/lib/silas/agent_sdk/version_guard.rb +26 -0
- data/lib/silas/budget.rb +42 -0
- data/lib/silas/channel.rb +69 -0
- data/lib/silas/configuration.rb +164 -0
- data/lib/silas/connection.rb +77 -0
- data/lib/silas/connections.rb +52 -0
- data/lib/silas/engine.rb +36 -0
- data/lib/silas/engines/agent_sdk.rb +75 -0
- data/lib/silas/engines/base.rb +30 -0
- data/lib/silas/engines/ruby_llm.rb +136 -0
- data/lib/silas/errors.rb +26 -0
- data/lib/silas/eval/assertions.rb +107 -0
- data/lib/silas/eval/driver.rb +47 -0
- data/lib/silas/eval/dsl.rb +55 -0
- data/lib/silas/eval/grader.rb +37 -0
- data/lib/silas/eval/result.rb +28 -0
- data/lib/silas/eval/runner.rb +38 -0
- data/lib/silas/eval/scripted_engine.rb +39 -0
- data/lib/silas/eval/transcript.rb +22 -0
- data/lib/silas/eval.rb +35 -0
- data/lib/silas/inbox/cost.rb +58 -0
- data/lib/silas/inbox.rb +23 -0
- data/lib/silas/instructions.rb +55 -0
- data/lib/silas/ledger.rb +178 -0
- data/lib/silas/mcp/client.rb +86 -0
- data/lib/silas/mcp/handler.rb +89 -0
- data/lib/silas/mcp/server.rb +134 -0
- data/lib/silas/message_builder.rb +63 -0
- data/lib/silas/nested_runner.rb +41 -0
- data/lib/silas/registry.rb +155 -0
- data/lib/silas/sandbox/docker.rb +67 -0
- data/lib/silas/sandbox/null.rb +17 -0
- data/lib/silas/sandbox.rb +15 -0
- data/lib/silas/schedule/compiler.rb +52 -0
- data/lib/silas/schedule.rb +109 -0
- data/lib/silas/skill.rb +21 -0
- data/lib/silas/slack.rb +55 -0
- data/lib/silas/step_runner.rb +98 -0
- data/lib/silas/subprocess_runner.rb +41 -0
- data/lib/silas/tool.rb +93 -0
- data/lib/silas/tools/delegate.rb +42 -0
- data/lib/silas/tools/load_skill.rb +25 -0
- data/lib/silas/tools/run_code.rb +21 -0
- data/lib/silas/version.rb +3 -0
- data/lib/silas.rb +144 -0
- data/lib/tasks/silas_eval.rake +20 -0
- data/lib/tasks/silas_schedules.rake +33 -0
- metadata +232 -0
data/lib/silas/tool.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Silas
|
|
2
|
+
# Base class for agent tools. The keyword signature of #call IS the schema:
|
|
3
|
+
#
|
|
4
|
+
# class Agent::Tools::IssueRefund < Silas::Tool
|
|
5
|
+
# description "Refund an order."
|
|
6
|
+
# param :amount, :integer, desc: "Pence" # optional type refinement
|
|
7
|
+
# approval :always # :never | :once | :always | lambda
|
|
8
|
+
# transactional! # or at_most_once! (default) / idempotent!
|
|
9
|
+
#
|
|
10
|
+
# def call(order_id:, amount:, note: nil)
|
|
11
|
+
# ...
|
|
12
|
+
# end
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Tool identity is the FILENAME (app/agent/tools/issue_refund.rb -> "issue_refund"),
|
|
16
|
+
# eve's convention, enforced by the Registry.
|
|
17
|
+
class Tool
|
|
18
|
+
class << self
|
|
19
|
+
def description(text = nil)
|
|
20
|
+
@description = text if text
|
|
21
|
+
@description || ""
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def param(name, type = :string, desc: nil)
|
|
25
|
+
param_refinements[name.to_sym] = { type: type.to_s, desc: desc }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def approval(policy = nil, &block)
|
|
29
|
+
@approval_policy = block || policy unless policy.nil? && block.nil?
|
|
30
|
+
@approval_policy || :never
|
|
31
|
+
end
|
|
32
|
+
alias approval_policy approval
|
|
33
|
+
|
|
34
|
+
def transactional! = @effect_mode = :transactional
|
|
35
|
+
def idempotent! = @effect_mode = :idempotent
|
|
36
|
+
def at_most_once! = @effect_mode = :at_most_once
|
|
37
|
+
|
|
38
|
+
def effect_mode = @effect_mode || :at_most_once
|
|
39
|
+
|
|
40
|
+
def tool_name
|
|
41
|
+
name.demodulize.underscore
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# {name:, description:, input_schema:} — provider-agnostic JSON schema.
|
|
45
|
+
def schema
|
|
46
|
+
validate_signature!
|
|
47
|
+
properties = {}
|
|
48
|
+
required = []
|
|
49
|
+
instance_method(:call).parameters.each do |kind, pname|
|
|
50
|
+
refinement = param_refinements[pname] || {}
|
|
51
|
+
properties[pname.to_s] = { "type" => refinement[:type] || "string" }
|
|
52
|
+
properties[pname.to_s]["description"] = refinement[:desc] if refinement[:desc]
|
|
53
|
+
required << pname.to_s if kind == :keyreq
|
|
54
|
+
end
|
|
55
|
+
{
|
|
56
|
+
"name" => tool_name,
|
|
57
|
+
"description" => description,
|
|
58
|
+
"input_schema" => {
|
|
59
|
+
"type" => "object",
|
|
60
|
+
"properties" => properties,
|
|
61
|
+
"required" => required
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_signature!
|
|
67
|
+
raise Error, "#{name} must define #call" unless method_defined?(:call)
|
|
68
|
+
|
|
69
|
+
kinds = instance_method(:call).parameters.map(&:first)
|
|
70
|
+
if kinds.any? { |k| %i[req opt rest].include?(k) }
|
|
71
|
+
raise Error, "#{name}#call must accept keyword arguments only — the signature is the schema"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def param_refinements
|
|
78
|
+
@param_refinements ||= {}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Instance-side delegation so a resolved instance answers the Ledger.
|
|
83
|
+
def approval_policy = self.class.approval_policy
|
|
84
|
+
def effect_mode = self.class.effect_mode
|
|
85
|
+
|
|
86
|
+
# Execution context, set by the Ledger before #call.
|
|
87
|
+
attr_accessor :session
|
|
88
|
+
|
|
89
|
+
def call(**)
|
|
90
|
+
raise NotImplementedError, "#{self.class}#call"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Silas
|
|
2
|
+
module Tools
|
|
3
|
+
# Built-in. Added to the ROOT toolset only when subagents exist; NOT given to
|
|
4
|
+
# subagents (depth-1, no recursion). The whole delegation is ONE external
|
|
5
|
+
# effect: at_most_once! means a crash mid-run parks the parent invocation
|
|
6
|
+
# in-doubt (the exactly-once boundary), never silently re-runs.
|
|
7
|
+
class Delegate < Silas::Tool
|
|
8
|
+
at_most_once!
|
|
9
|
+
approval :never
|
|
10
|
+
param :subagent, :string, desc: "Which subagent to run (one of the names listed above)."
|
|
11
|
+
param :input, :string, desc: "Full, self-contained task — the subagent shares NONE of your context."
|
|
12
|
+
|
|
13
|
+
def self.tool_name = "delegate"
|
|
14
|
+
|
|
15
|
+
# Dynamic description enumerates the roster, so the roster lands in the ROOT
|
|
16
|
+
# digest (schema -> Registry#definitions -> digest). Adding a subagent
|
|
17
|
+
# mid-turn => NondeterminismError, exactly like a skill change.
|
|
18
|
+
def self.description
|
|
19
|
+
roster = Silas.subagent_index
|
|
20
|
+
base = "Delegate a self-contained subtask to a specialized subagent that runs with its own " \
|
|
21
|
+
"instructions, tools, and a fresh context, then returns its final answer."
|
|
22
|
+
return base if roster.empty?
|
|
23
|
+
|
|
24
|
+
base + "\n\nAvailable subagents:\n" + roster.map { |n, d| "- #{n}: #{d}" }.join("\n")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# `session` is the PARENT session, set by the Ledger before #call.
|
|
28
|
+
def call(subagent:, input:)
|
|
29
|
+
return { "error" => "unknown subagent #{subagent.inspect}" } unless Silas.subagent?(subagent)
|
|
30
|
+
|
|
31
|
+
nested = Silas::Session.create!(agent_name: subagent, parent_session_id: session.id,
|
|
32
|
+
metadata: { "delegated_from" => session.id })
|
|
33
|
+
turn = Silas::NestedRunner.run(nested, input: input)
|
|
34
|
+
if turn.completed?
|
|
35
|
+
{ "session_id" => nested.id, "answer" => turn.answer_text }
|
|
36
|
+
else
|
|
37
|
+
{ "session_id" => nested.id, "error" => "subagent did not finish (#{turn.status}: #{turn.failure_reason})" }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Silas
|
|
2
|
+
module Tools
|
|
3
|
+
# The built-in progressive-disclosure tool (eve's mechanism): skill
|
|
4
|
+
# descriptions are advertised in the instructions; the body loads on
|
|
5
|
+
# demand and — because the body IS the tool result — rides the ledger and
|
|
6
|
+
# replays byte-identically for free. Loading also registers the skill on
|
|
7
|
+
# the session so subsequent turns inline it directly.
|
|
8
|
+
class LoadSkill < Tool
|
|
9
|
+
description "Load the full playbook for a named skill. Use when a task matches a skill description."
|
|
10
|
+
transactional!
|
|
11
|
+
|
|
12
|
+
def self.tool_name = "load_skill"
|
|
13
|
+
|
|
14
|
+
def call(name:)
|
|
15
|
+
skill = Silas.skills.find { |s| s.name == name }
|
|
16
|
+
return { "error" => "unknown skill #{name.inspect}" } unless skill
|
|
17
|
+
|
|
18
|
+
unless session.loaded_skills.include?(name)
|
|
19
|
+
session.update!(loaded_skills: session.loaded_skills + [ name ])
|
|
20
|
+
end
|
|
21
|
+
{ "skill" => name, "content" => skill.body }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Silas
|
|
2
|
+
module Tools
|
|
3
|
+
# Built-in, added to the toolset only when a sandbox is configured. Runs a
|
|
4
|
+
# shell command through the configured sandbox. at_most_once! — a sandbox
|
|
5
|
+
# exec is an external effect, so a crash mid-run parks it in-doubt.
|
|
6
|
+
class RunCode < Tool
|
|
7
|
+
description "Run a shell command in an ephemeral, network-isolated sandbox container. " \
|
|
8
|
+
"Use for untrusted or model-generated code."
|
|
9
|
+
param :command, :string, desc: "Shell command run via /bin/sh -c inside the sandbox."
|
|
10
|
+
param :timeout, :integer, desc: "Max wall-clock seconds before the container is killed."
|
|
11
|
+
at_most_once!
|
|
12
|
+
|
|
13
|
+
def self.tool_name = "run_code"
|
|
14
|
+
|
|
15
|
+
def call(command:, timeout: nil)
|
|
16
|
+
result = Silas.resolved_sandbox.run(command, timeout: timeout || Silas.config.sandbox_timeout)
|
|
17
|
+
{ "stdout" => result.stdout, "stderr" => result.stderr, "exit_status" => result.exit_status }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/silas.rb
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Active Job (Continuations) and Active Record are the chassis — load them
|
|
2
|
+
# even in --minimal host apps that skipped the railties.
|
|
3
|
+
require "active_job/railtie" if defined?(::Rails::Railtie)
|
|
4
|
+
require "active_record/railtie" if defined?(::Rails::Railtie)
|
|
5
|
+
|
|
6
|
+
require "silas/version"
|
|
7
|
+
require "silas/errors"
|
|
8
|
+
require "silas/configuration"
|
|
9
|
+
require "silas/ledger"
|
|
10
|
+
require "silas/tool"
|
|
11
|
+
require "silas/skill"
|
|
12
|
+
require "silas/schedule"
|
|
13
|
+
require "silas/schedule/compiler"
|
|
14
|
+
require "silas/agent_scope"
|
|
15
|
+
require "silas/tools/delegate"
|
|
16
|
+
require "silas/nested_runner"
|
|
17
|
+
require "silas/sandbox"
|
|
18
|
+
require "silas/tools/run_code"
|
|
19
|
+
require "silas/channel"
|
|
20
|
+
require "silas/slack"
|
|
21
|
+
require "silas/mcp/client"
|
|
22
|
+
require "silas/connection"
|
|
23
|
+
require "silas/connections"
|
|
24
|
+
require "silas/inbox"
|
|
25
|
+
require "silas/inbox/cost"
|
|
26
|
+
require "silas/budget"
|
|
27
|
+
require "silas/registry"
|
|
28
|
+
require "silas/agent"
|
|
29
|
+
require "silas/tools/load_skill"
|
|
30
|
+
require "silas/engines/base"
|
|
31
|
+
require "silas/agent_sdk/version_guard"
|
|
32
|
+
require "silas/agent_sdk/stream_parser"
|
|
33
|
+
require "silas/agent_sdk/cli"
|
|
34
|
+
require "silas/mcp/handler"
|
|
35
|
+
require "silas/mcp/server"
|
|
36
|
+
require "silas/engines/agent_sdk"
|
|
37
|
+
require "ruby_llm"
|
|
38
|
+
require "silas/engines/ruby_llm"
|
|
39
|
+
require "silas/message_builder"
|
|
40
|
+
require "silas/instructions"
|
|
41
|
+
require "silas/step_runner"
|
|
42
|
+
require "silas/subprocess_runner"
|
|
43
|
+
require "silas/eval" # after engines (ScriptedEngine < Engines::Base)
|
|
44
|
+
|
|
45
|
+
module Silas
|
|
46
|
+
class << self
|
|
47
|
+
def config
|
|
48
|
+
@config ||= Configuration.new
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def configure
|
|
52
|
+
yield config
|
|
53
|
+
config.validate!
|
|
54
|
+
config
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def reset_configuration! # for specs
|
|
58
|
+
@config = nil
|
|
59
|
+
@resolved_engine = nil
|
|
60
|
+
@resolved_sandbox = nil
|
|
61
|
+
@agent = nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def sandbox_enabled? = ![ :none, nil ].include?(config.sandbox)
|
|
65
|
+
|
|
66
|
+
def resolved_sandbox
|
|
67
|
+
@resolved_sandbox ||=
|
|
68
|
+
case config.sandbox
|
|
69
|
+
when :none, nil then Sandbox::Null.new
|
|
70
|
+
when :docker
|
|
71
|
+
Sandbox::Docker.new(image: config.sandbox_image, network: config.sandbox_network,
|
|
72
|
+
memory: config.sandbox_memory, cpus: config.sandbox_cpus,
|
|
73
|
+
pids: config.sandbox_pids, workdir: config.sandbox_workdir,
|
|
74
|
+
docker_bin: config.sandbox_docker_bin)
|
|
75
|
+
when Symbol then raise Error, "unknown sandbox #{config.sandbox.inspect}"
|
|
76
|
+
else config.sandbox
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def reset_agent_memo! = (@agent = nil) # after Registry.install! swaps dirs
|
|
81
|
+
|
|
82
|
+
# The inference adapter instance. config.engine may be a symbol (:ruby_llm,
|
|
83
|
+
# :agent_sdk) or any object responding to #execute_step (specs, custom).
|
|
84
|
+
def resolved_engine
|
|
85
|
+
@resolved_engine ||=
|
|
86
|
+
case config.engine
|
|
87
|
+
when :ruby_llm then Engines::RubyLLM.new
|
|
88
|
+
when :agent_sdk then Engines::AgentSdk.new
|
|
89
|
+
when Symbol then raise Error, "unknown engine #{config.engine.inspect}"
|
|
90
|
+
else config.engine
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def tool_resolver
|
|
95
|
+
config.tool_resolver or raise Error, "no tool resolver configured (Registry boots one; specs must inject)"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def tool_definitions
|
|
99
|
+
config.tool_definitions&.call || []
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def skills
|
|
103
|
+
config.skills&.call || []
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def schedules
|
|
107
|
+
config.schedules&.call || []
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The active agent definition. config.agent_override is set during a nested
|
|
111
|
+
# subagent run; otherwise it's the root app/agent.
|
|
112
|
+
def agent
|
|
113
|
+
config.agent_override || (@agent ||= Agent.load)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Subagent roster: [[name, description], ...] (model-visible, so it's in the
|
|
117
|
+
# root digest via Delegate.description).
|
|
118
|
+
def subagent_index = config.subagent_index&.call || []
|
|
119
|
+
def subagent?(name) = subagent_index.any? { |n, _| n == name.to_s }
|
|
120
|
+
def subagent_scope(name) = config.subagent_scopes&.call&.fetch(name.to_s)
|
|
121
|
+
|
|
122
|
+
# Run a block with a subagent's scope swapped in as the active globals,
|
|
123
|
+
# restoring afterward. Synchronous/depth-1 — safe because delegation runs
|
|
124
|
+
# inline on one thread while the parent loop is paused in the delegate tool.
|
|
125
|
+
def with_agent_scope(scope)
|
|
126
|
+
saved = {
|
|
127
|
+
tool_resolver: config.tool_resolver, tool_definitions: config.tool_definitions,
|
|
128
|
+
definitions_digest: config.definitions_digest, skills: config.skills,
|
|
129
|
+
agent_override: config.agent_override, instructions_dir: config.instructions_dir
|
|
130
|
+
}
|
|
131
|
+
config.tool_resolver = scope.resolver
|
|
132
|
+
config.tool_definitions = -> { scope.definitions }
|
|
133
|
+
config.definitions_digest = -> { scope.digest }
|
|
134
|
+
config.skills = -> { scope.skills }
|
|
135
|
+
config.agent_override = scope.agent
|
|
136
|
+
config.instructions_dir = scope.dir
|
|
137
|
+
yield
|
|
138
|
+
ensure
|
|
139
|
+
saved.each { |k, v| config.public_send("#{k}=", v) }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
require "silas/engine" if defined?(::Rails::Engine)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
namespace :silas do
|
|
2
|
+
desc "Run agent evals as a deploy gate (exit 1 on failure). SILAS_EVAL_MODE=real for live; EVAL=regex to filter."
|
|
3
|
+
task eval: :environment do
|
|
4
|
+
ok = Silas::Eval::Runner.run(
|
|
5
|
+
dir: ENV["SILAS_EVAL_DIR"] || Silas.config.eval_dir,
|
|
6
|
+
pattern: (Regexp.new(ENV["EVAL"]) if ENV["EVAL"]),
|
|
7
|
+
mode: ENV["SILAS_EVAL_MODE"]&.to_sym
|
|
8
|
+
)
|
|
9
|
+
exit(1) unless ok
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
namespace :eval do
|
|
13
|
+
desc "List discovered eval scenarios without running them"
|
|
14
|
+
task list: :environment do
|
|
15
|
+
Silas::Eval.reset!
|
|
16
|
+
Dir[File.join(Silas.config.eval_dir, "**/*_eval.rb")].sort.each { |f| load f }
|
|
17
|
+
Silas::Eval.scenarios.each { |s| puts " [#{s.mode}] #{s.name}" }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
namespace :silas do
|
|
2
|
+
desc "Compile app/agent/schedules/** into config/recurring.yml (env: RAILS_ENV or ENV[SILAS_SCHEDULE_ENV] or production)"
|
|
3
|
+
task schedules: :environment do
|
|
4
|
+
Silas::Registry.install!(root: Rails.root)
|
|
5
|
+
env = ENV["SILAS_SCHEDULE_ENV"] || "production"
|
|
6
|
+
schedules = Silas.schedules
|
|
7
|
+
Silas::Schedule::Compiler.write!(schedules, root: Rails.root, env: env)
|
|
8
|
+
if schedules.empty?
|
|
9
|
+
puts "silas: no schedules found under app/agent/schedules/ (managed entries cleared for #{env})"
|
|
10
|
+
else
|
|
11
|
+
puts "silas: compiled #{schedules.size} schedule(s) into config/recurring.yml [#{env}]:"
|
|
12
|
+
schedules.sort_by(&:name).each { |s| puts " #{s.recurring_key} #{s.cron} (#{s.kind}) -> #{s.name}" }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
namespace :schedules do
|
|
17
|
+
desc "Report drift between discovered schedules and config/recurring.yml"
|
|
18
|
+
task list: :environment do
|
|
19
|
+
Silas::Registry.install!(root: Rails.root)
|
|
20
|
+
env = ENV["SILAS_SCHEDULE_ENV"] || "production"
|
|
21
|
+
drift = Silas::Schedule::Compiler.drift(Silas.schedules, root: Rails.root, env: env)
|
|
22
|
+
puts "silas schedules [#{env}]:"
|
|
23
|
+
puts " discovered: #{Silas.schedules.map(&:name).sort.join(', ').then { |s| s.empty? ? '(none)' : s }}"
|
|
24
|
+
unless drift[:uncompiled].empty?
|
|
25
|
+
puts " NOT COMPILED (run `bin/rails silas:schedules`): #{drift[:uncompiled].join(', ')}"
|
|
26
|
+
end
|
|
27
|
+
unless drift[:orphaned].empty?
|
|
28
|
+
puts " ORPHANED in recurring.yml (file deleted): #{drift[:orphaned].join(', ')}"
|
|
29
|
+
end
|
|
30
|
+
puts " in sync." if drift[:uncompiled].empty? && drift[:orphaned].empty?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: silas
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Daniel St Paul
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ruby_llm
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec-rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '7.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '7.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: sqlite3
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: pg
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 1.5.0
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 1.5.0
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: solid_queue
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.2'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.2'
|
|
96
|
+
description: 'An agent framework where your Rails app is the chassis: app/agent/ directory
|
|
97
|
+
convention, durable runs on Active Job Continuations, exactly-once tool execution
|
|
98
|
+
via a transactional ledger, approvals that park at zero compute.'
|
|
99
|
+
email:
|
|
100
|
+
- daniel@zerogravity.co.uk
|
|
101
|
+
executables: []
|
|
102
|
+
extensions: []
|
|
103
|
+
extra_rdoc_files: []
|
|
104
|
+
files:
|
|
105
|
+
- CHANGELOG.md
|
|
106
|
+
- LICENSE
|
|
107
|
+
- README.md
|
|
108
|
+
- app/controllers/silas/channels/approvals_controller.rb
|
|
109
|
+
- app/controllers/silas/channels/base_controller.rb
|
|
110
|
+
- app/controllers/silas/channels/slack_controller.rb
|
|
111
|
+
- app/controllers/silas/inbox/base_controller.rb
|
|
112
|
+
- app/controllers/silas/inbox/invocations_controller.rb
|
|
113
|
+
- app/controllers/silas/inbox/sessions_controller.rb
|
|
114
|
+
- app/helpers/silas/inbox/trace_helper.rb
|
|
115
|
+
- app/jobs/silas/agent_loop_job.rb
|
|
116
|
+
- app/jobs/silas/channel_delivery_job.rb
|
|
117
|
+
- app/jobs/silas/dead_job_rescuer_job.rb
|
|
118
|
+
- app/jobs/silas/schedule_job.rb
|
|
119
|
+
- app/mailboxes/silas/agent_mailbox.rb
|
|
120
|
+
- app/mailers/silas/channel_mailer.rb
|
|
121
|
+
- app/models/concerns/silas/inbox/broadcastable.rb
|
|
122
|
+
- app/models/silas/application_record.rb
|
|
123
|
+
- app/models/silas/session.rb
|
|
124
|
+
- app/models/silas/step.rb
|
|
125
|
+
- app/models/silas/tool_invocation.rb
|
|
126
|
+
- app/models/silas/turn.rb
|
|
127
|
+
- app/views/layouts/silas/inbox.html.erb
|
|
128
|
+
- app/views/silas/channel_mailer/approval.text.erb
|
|
129
|
+
- app/views/silas/channels/approvals/done.html.erb
|
|
130
|
+
- app/views/silas/channels/approvals/error.html.erb
|
|
131
|
+
- app/views/silas/channels/approvals/show.html.erb
|
|
132
|
+
- app/views/silas/inbox/invocations/_approval_card.html.erb
|
|
133
|
+
- app/views/silas/inbox/invocations/_invocation.html.erb
|
|
134
|
+
- app/views/silas/inbox/sessions/_cost.html.erb
|
|
135
|
+
- app/views/silas/inbox/sessions/index.html.erb
|
|
136
|
+
- app/views/silas/inbox/sessions/show.html.erb
|
|
137
|
+
- app/views/silas/inbox/steps/_step.html.erb
|
|
138
|
+
- app/views/silas/inbox/turns/_header.html.erb
|
|
139
|
+
- app/views/silas/inbox/turns/_turn.html.erb
|
|
140
|
+
- config/routes.rb
|
|
141
|
+
- db/migrate/20260714000001_create_silas_tables.rb
|
|
142
|
+
- db/migrate/20260715000001_add_channel_outbound_markers.rb
|
|
143
|
+
- db/migrate/20260715000002_add_agent_sdk_columns_to_turns.rb
|
|
144
|
+
- db/migrate/20260715000003_add_parent_session_to_silas_sessions.rb
|
|
145
|
+
- lib/generators/silas/install/install_generator.rb
|
|
146
|
+
- lib/generators/silas/install/templates/agent.yml
|
|
147
|
+
- lib/generators/silas/install/templates/bin_ci
|
|
148
|
+
- lib/generators/silas/install/templates/channel_email.rb
|
|
149
|
+
- lib/generators/silas/install/templates/channel_slack.rb
|
|
150
|
+
- lib/generators/silas/install/templates/example_eval.rb
|
|
151
|
+
- lib/generators/silas/install/templates/example_schedule.md
|
|
152
|
+
- lib/generators/silas/install/templates/example_skill.md
|
|
153
|
+
- lib/generators/silas/install/templates/example_tool.rb
|
|
154
|
+
- lib/generators/silas/install/templates/initializer.rb
|
|
155
|
+
- lib/generators/silas/install/templates/instructions.md
|
|
156
|
+
- lib/silas.rb
|
|
157
|
+
- lib/silas/agent.rb
|
|
158
|
+
- lib/silas/agent_scope.rb
|
|
159
|
+
- lib/silas/agent_sdk/cli.rb
|
|
160
|
+
- lib/silas/agent_sdk/stream_parser.rb
|
|
161
|
+
- lib/silas/agent_sdk/version_guard.rb
|
|
162
|
+
- lib/silas/budget.rb
|
|
163
|
+
- lib/silas/channel.rb
|
|
164
|
+
- lib/silas/configuration.rb
|
|
165
|
+
- lib/silas/connection.rb
|
|
166
|
+
- lib/silas/connections.rb
|
|
167
|
+
- lib/silas/engine.rb
|
|
168
|
+
- lib/silas/engines/agent_sdk.rb
|
|
169
|
+
- lib/silas/engines/base.rb
|
|
170
|
+
- lib/silas/engines/ruby_llm.rb
|
|
171
|
+
- lib/silas/errors.rb
|
|
172
|
+
- lib/silas/eval.rb
|
|
173
|
+
- lib/silas/eval/assertions.rb
|
|
174
|
+
- lib/silas/eval/driver.rb
|
|
175
|
+
- lib/silas/eval/dsl.rb
|
|
176
|
+
- lib/silas/eval/grader.rb
|
|
177
|
+
- lib/silas/eval/result.rb
|
|
178
|
+
- lib/silas/eval/runner.rb
|
|
179
|
+
- lib/silas/eval/scripted_engine.rb
|
|
180
|
+
- lib/silas/eval/transcript.rb
|
|
181
|
+
- lib/silas/inbox.rb
|
|
182
|
+
- lib/silas/inbox/cost.rb
|
|
183
|
+
- lib/silas/instructions.rb
|
|
184
|
+
- lib/silas/ledger.rb
|
|
185
|
+
- lib/silas/mcp/client.rb
|
|
186
|
+
- lib/silas/mcp/handler.rb
|
|
187
|
+
- lib/silas/mcp/server.rb
|
|
188
|
+
- lib/silas/message_builder.rb
|
|
189
|
+
- lib/silas/nested_runner.rb
|
|
190
|
+
- lib/silas/registry.rb
|
|
191
|
+
- lib/silas/sandbox.rb
|
|
192
|
+
- lib/silas/sandbox/docker.rb
|
|
193
|
+
- lib/silas/sandbox/null.rb
|
|
194
|
+
- lib/silas/schedule.rb
|
|
195
|
+
- lib/silas/schedule/compiler.rb
|
|
196
|
+
- lib/silas/skill.rb
|
|
197
|
+
- lib/silas/slack.rb
|
|
198
|
+
- lib/silas/step_runner.rb
|
|
199
|
+
- lib/silas/subprocess_runner.rb
|
|
200
|
+
- lib/silas/tool.rb
|
|
201
|
+
- lib/silas/tools/delegate.rb
|
|
202
|
+
- lib/silas/tools/load_skill.rb
|
|
203
|
+
- lib/silas/tools/run_code.rb
|
|
204
|
+
- lib/silas/version.rb
|
|
205
|
+
- lib/tasks/silas_eval.rake
|
|
206
|
+
- lib/tasks/silas_schedules.rake
|
|
207
|
+
homepage: https://github.com/danielstpaul/silas
|
|
208
|
+
licenses:
|
|
209
|
+
- MIT
|
|
210
|
+
metadata:
|
|
211
|
+
homepage_uri: https://github.com/danielstpaul/silas
|
|
212
|
+
source_code_uri: https://github.com/danielstpaul/silas
|
|
213
|
+
changelog_uri: https://github.com/danielstpaul/silas/blob/main/CHANGELOG.md
|
|
214
|
+
rubygems_mfa_required: 'true'
|
|
215
|
+
rdoc_options: []
|
|
216
|
+
require_paths:
|
|
217
|
+
- lib
|
|
218
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
219
|
+
requirements:
|
|
220
|
+
- - ">="
|
|
221
|
+
- !ruby/object:Gem::Version
|
|
222
|
+
version: '3.2'
|
|
223
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
224
|
+
requirements:
|
|
225
|
+
- - ">="
|
|
226
|
+
- !ruby/object:Gem::Version
|
|
227
|
+
version: '0'
|
|
228
|
+
requirements: []
|
|
229
|
+
rubygems_version: 3.6.9
|
|
230
|
+
specification_version: 4
|
|
231
|
+
summary: Durable AI agents on the Rails you already run.
|
|
232
|
+
test_files: []
|