silas 0.5.0 → 0.6.0
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 +101 -0
- data/DEPLOY.md +111 -0
- data/README.md +87 -256
- data/app/helpers/silas/inbox/trace_helper.rb +20 -5
- data/app/models/concerns/silas/inbox/broadcastable.rb +12 -0
- data/app/views/layouts/silas/inbox.html.erb +89 -30
- data/app/views/silas/inbox/invocations/_approval_card.html.erb +11 -2
- data/app/views/silas/inbox/invocations/_invocation.html.erb +17 -5
- data/app/views/silas/inbox/sessions/_row.html.erb +14 -0
- data/app/views/silas/inbox/sessions/index.html.erb +16 -15
- data/app/views/silas/inbox/sessions/show.html.erb +9 -0
- data/docs/agents.md +81 -0
- data/docs/budgets.md +67 -0
- data/docs/cancellation.md +41 -0
- data/docs/channels.md +290 -0
- data/docs/configuration.md +106 -0
- data/docs/connections.md +55 -0
- data/docs/conventions.md +161 -0
- data/docs/evals.md +95 -0
- data/docs/guarantees.md +76 -0
- data/docs/inbox-and-api.md +84 -0
- data/docs/memory.md +35 -0
- data/docs/sandbox.md +44 -0
- data/docs/tools.md +77 -0
- data/docs/tutorial.md +268 -0
- data/docs/vs-eve.md +93 -0
- data/docs/why-silas.md +87 -0
- data/lib/generators/silas/install/install_generator.rb +9 -0
- data/lib/generators/silas/install/templates/claude_skill.md +136 -0
- data/lib/generators/silas/install/templates/ruby_llm.rb +4 -1
- data/lib/silas/eval/dsl.rb +7 -2
- data/lib/silas/version.rb +1 -1
- metadata +22 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd68f6d368466b20fbb35f49628f166ac61dd60ace3a8cbd20bbc6e316c1d346
|
|
4
|
+
data.tar.gz: 94d2a902cd7f364aa33200b1ea1f98446dabee38d9e9f7431df9cd96d1fc7f41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c386608b076c2c3dfb23e369c819a7d7b1c4fc6122f0011ea80880a46b158b25549a75388a17de250b200a60e0ada311a73df28b47c67e4007cc9e2ebef8bf3f
|
|
7
|
+
data.tar.gz: 4216b83c6925605076cb3742286699673e2a18dfd9eab3a3cc6e21479ee64e17de714737e5764acce0f147ca0023ced60ed0864b0836850c771dbab88ea84100
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,106 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 (2026-07-26)
|
|
4
|
+
|
|
5
|
+
The brand release: the Signals inbox, the docs surface (site + gem-shipped
|
|
6
|
+
guides + tutorial), the templates family (desk + analyst) with its anti-rot
|
|
7
|
+
CI gate, community files, and the framework-first README.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Docs for the whole surface.** New guides, all shipped in the gem:
|
|
12
|
+
`tutorial` (build the refund desk outward, one primitive per chapter),
|
|
13
|
+
`guarantees`, `tools`, `agents`, `memory`, `inbox-and-api`, `sandbox`,
|
|
14
|
+
`evals`, `budgets`, `cancellation`, `connections`, and a full
|
|
15
|
+
`configuration` reference. A docs site (Jekyll + just-the-docs, dark-first,
|
|
16
|
+
Archivo/Space Mono, `early · 0.x` chip) builds from `docs/` in CI and
|
|
17
|
+
deploys to Pages, with an `llms.txt` for coding agents. Brand assets
|
|
18
|
+
(`brand/`, hero at `docs/img/`) now live in the repo; the hero image is
|
|
19
|
+
excluded from the packaged gem.
|
|
20
|
+
- **Rails application templates, as a family** — `templates/desk.rb` and
|
|
21
|
+
`templates/analyst.rb`, each a single CI-gated `rails new -m` script.
|
|
22
|
+
`rails new desk -m
|
|
23
|
+
https://raw.githubusercontent.com/danielstpaul/silas/main/templates/desk.rb`
|
|
24
|
+
builds a deployable agent app from nothing: a refund desk with one tool per effect
|
|
25
|
+
mode (`lookup_order` idempotent, `issue_refund` transactional behind a
|
|
26
|
+
£25 approval gate, `notify_customer` at-most-once), Solid Queue **and** Solid
|
|
27
|
+
Cable wired in development (the durability contract needs a real worker; live
|
|
28
|
+
deltas need a cross-process cable), a keyless scripted stand-in so the first
|
|
29
|
+
`bin/dev` works with zero secrets, three deterministic agent evals asserting
|
|
30
|
+
the hold and the exactly-once execution, and a Signals-branded signal-board
|
|
31
|
+
landing page. The template only runs the generators the gem already tests,
|
|
32
|
+
and the `templates_smoke` workflow regenerates and tests an app from every
|
|
33
|
+
template on each push — starters that structurally cannot rot. (eve needs
|
|
34
|
+
template repos because it has no generator story; Rails has application
|
|
35
|
+
templates.) **The analyst** is the second template: a scheduled reporting
|
|
36
|
+
agent — `query_metrics` reads, `flag_anomaly` lands rows exactly once,
|
|
37
|
+
`publish_report` holds at the signal (`approval :always`), a Monday-07:00
|
|
38
|
+
schedule, and a schema-checked `final_answer` (`Turn#answer_data`). Both
|
|
39
|
+
templates also wire `ANTHROPIC_API_KEY` into Kamal's secrets so the
|
|
40
|
+
generated app deploys without a scavenger hunt.
|
|
41
|
+
- **Community files** — `CONTRIBUTING.md` (the deliberate scope no-list, the
|
|
42
|
+
chaos-gate requirement, how templates are contributed), `SECURITY.md`
|
|
43
|
+
(private vulnerability reporting; deny-by-default surfaces; contract
|
|
44
|
+
violations count), and GitHub issue forms.
|
|
45
|
+
- **The gem ships its own docs, and the installer ships a coding-agent skill.**
|
|
46
|
+
`docs/**` and `DEPLOY.md` are in the packaged gem, so `bundle show silas`
|
|
47
|
+
gives a coding agent (or an offline human) the real reference — and the
|
|
48
|
+
README's links stop 404ing for gem-only readers. `rails g silas:install` now
|
|
49
|
+
also writes `.claude/skills/silas/SKILL.md`: the `app/agent/` conventions,
|
|
50
|
+
the effect-mode and approval decision rules, and the ledger rules an agent
|
|
51
|
+
must never violate — so a Claude Code/Codex session building on this app
|
|
52
|
+
gets the framework's judgment without the human learning it first. Gemspec
|
|
53
|
+
gains `documentation_uri` and `bug_tracker_uri`.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- **The inbox wears the brand** (direction "Signals"). Dark-first — the tokens'
|
|
58
|
+
base is the night palette and light is the `prefers-color-scheme` override —
|
|
59
|
+
with a position-light mark, a lowercase wordmark, and one white "lamp" accent
|
|
60
|
+
that never means state. The seven run states each get their aspect:
|
|
61
|
+
`in_doubt` its own violet (it is neither waiting-by-design nor failed),
|
|
62
|
+
`canceled` a dashed quiet (a lamp going out, not turning red), and two
|
|
63
|
+
UI-only relabels — `waiting` reads **held**, `completed` reads **clear**. The
|
|
64
|
+
database strings and the JSON API are untouched (`docs/conventions.md`).
|
|
65
|
+
- **Approvals and questions are hoisted to the top of the session** — the
|
|
66
|
+
operator never scrolls a long trace hunting for the card; the trace keeps a
|
|
67
|
+
one-line "held at the signal" stub in place. Live parks append their card via
|
|
68
|
+
the same Turbo broadcasts; settled cards remove themselves. Tool arguments
|
|
69
|
+
render as key/value rows and results collapse behind a disclosure; the
|
|
70
|
+
session index groups its rail by who's blocked: **Held / Working / Filed**.
|
|
71
|
+
- The playground's chat page consumes the engine's colour tokens instead of
|
|
72
|
+
re-hardcoding a palette, and hosts the hoisted approval/question cards above
|
|
73
|
+
its transcript.
|
|
74
|
+
|
|
75
|
+
- **README reframed, framework-first** — it now mirrors eve's shape exactly:
|
|
76
|
+
the mark as the logo (dark/light `<picture>`), badges, a one-paragraph
|
|
77
|
+
thesis, "The filesystem is the authoring interface" with the tree, quick
|
|
78
|
+
start, one minimal example, status/community/license. The hero illustration
|
|
79
|
+
moved to the repo's social-preview role (its inbox panel is a designed
|
|
80
|
+
idealization, not a screenshot — it shouldn't sit where users compare it to
|
|
81
|
+
the real `/silas/inbox`). The docs site nav is grouped eve-style — Tutorial
|
|
82
|
+
and Guarantees up top, then **Core / Advanced / Reference / About**
|
|
83
|
+
sections — via build-time frontmatter injection (`site/assemble.rb`, which
|
|
84
|
+
fails the build if a docs page lacks a nav entry), keeping the gem-shipped
|
|
85
|
+
markdown frontmatter-free. `docs/vs-eve.md` was **factually rewritten**
|
|
86
|
+
(2026-07-26): the previous revision described eve as a managed-cloud
|
|
87
|
+
platform; eve is fully self-hostable, and the comparison now rests on what's
|
|
88
|
+
actually different — exactly-once vs documented at-least-once, a shipped
|
|
89
|
+
production inbox vs a dev TUI, memory vs none, and the transaction-boundary
|
|
90
|
+
argument, date-stamped against eve 0.27.6 and framed as siblings (pick by
|
|
91
|
+
stack). `docs/why-silas.md` rewritten in the same voice — you build the same
|
|
92
|
+
things with Silas you'd build with any modern agent framework; the
|
|
93
|
+
guarantees are where it goes further — and DEPLOY.md dropped its stale "eve
|
|
94
|
+
without the bill" subtitle.
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- The generated `config/initializers/ruby_llm.rb` no longer guards its whole
|
|
99
|
+
configure block behind `ANTHROPIC_API_KEY` — keyless boots skipped the
|
|
100
|
+
`use_new_acts_as` opt-in, so every demo-mode boot printed RubyLLM's legacy
|
|
101
|
+
acts_as deprecation warning. A nil key assignment is inert; the block now
|
|
102
|
+
always runs.
|
|
103
|
+
|
|
3
104
|
## 0.5.0 (2026-07-26)
|
|
4
105
|
|
|
5
106
|
Two new loop primitives (replay-safe compaction, ask_question), a whole-channel
|
data/DEPLOY.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Deploying Silas
|
|
2
|
+
|
|
3
|
+
A Silas app is an ordinary Rails app, so it deploys the way your Rails apps
|
|
4
|
+
already deploy — a single cheap VPS with Kamal works fine; there is no second
|
|
5
|
+
runtime or control plane to stand up. The one thing you MUST get right: the
|
|
6
|
+
**Solid Queue worker has to run**, because the durability contract (a turn
|
|
7
|
+
surviving a crash / deploy) depends on it. A web-only deploy will accept turns
|
|
8
|
+
and never run them.
|
|
9
|
+
|
|
10
|
+
> Status: this is the reference configuration. It is correct by construction
|
|
11
|
+
> (worker wired, rescuer active, secrets injected) but has not yet been run
|
|
12
|
+
> against a live VPS end-to-end — that's the final launch step.
|
|
13
|
+
|
|
14
|
+
## 1. The worker must run
|
|
15
|
+
|
|
16
|
+
> **Queue adapter: use Solid Queue.** Silas's durability and exactly-once
|
|
17
|
+
> guarantees require a durable, serializing, DB-backed adapter. **Do not run on
|
|
18
|
+
> the in-process `:async` adapter** (Rails' development default) — it runs
|
|
19
|
+
> continuation retries on a thread pool concurrently with the original job,
|
|
20
|
+
> which double-executes steps and breaks exactly-once. Silas warns at boot if it
|
|
21
|
+
> detects the Async adapter. For scripts and single-process demos, the
|
|
22
|
+
> synchronous `:inline` adapter is safe (but has no durability).
|
|
23
|
+
|
|
24
|
+
`rails g silas:install` already appends the durability rescuer to
|
|
25
|
+
`config/recurring.yml`:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
production:
|
|
29
|
+
silas_dead_job_rescuer: # retries jobs failed by dead-worker reaping/pruning
|
|
30
|
+
class: Silas::DeadJobRescuerJob
|
|
31
|
+
schedule: every 30 seconds
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pick one way to run Solid Queue:
|
|
35
|
+
|
|
36
|
+
- **Simple (one container):** run the worker inside Puma —
|
|
37
|
+
set `SOLID_QUEUE_IN_PUMA=true` (Rails 8 default plugin). Fine for a demo box.
|
|
38
|
+
- **Recommended (durable):** a dedicated `bin/jobs` process, so a `kill -9` of the
|
|
39
|
+
worker is isolated from the web tier and recovery is observable.
|
|
40
|
+
|
|
41
|
+
## 2. Kamal config (`config/deploy.yml`)
|
|
42
|
+
|
|
43
|
+
A standard `rails new` ships a Dockerfile and `config/deploy.yml`. Add a **job
|
|
44
|
+
role** so the worker is its own container:
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
service: my-agent
|
|
48
|
+
image: you/my-agent
|
|
49
|
+
servers:
|
|
50
|
+
web:
|
|
51
|
+
- 1.2.3.4 # your £5/month VPS
|
|
52
|
+
job:
|
|
53
|
+
hosts:
|
|
54
|
+
- 1.2.3.4
|
|
55
|
+
cmd: bin/jobs # the Solid Queue worker (+ dispatcher + scheduler)
|
|
56
|
+
|
|
57
|
+
registry:
|
|
58
|
+
username: you
|
|
59
|
+
password:
|
|
60
|
+
- KAMAL_REGISTRY_PASSWORD
|
|
61
|
+
|
|
62
|
+
env:
|
|
63
|
+
secret:
|
|
64
|
+
- RAILS_MASTER_KEY
|
|
65
|
+
- ANTHROPIC_API_KEY # the :ruby_llm engine authenticates with this
|
|
66
|
+
clear:
|
|
67
|
+
SOLID_QUEUE_IN_PUMA: false # the job role runs the worker instead
|
|
68
|
+
|
|
69
|
+
# Persist SQLite + Solid Queue on a volume if you're using SQLite (or point at Postgres).
|
|
70
|
+
volumes:
|
|
71
|
+
- "my_agent_storage:/rails/storage"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Secrets (Slack signing secret / bot token, model keys) go in `.kamal/secrets`
|
|
75
|
+
(pulled from your env or a vault), never in the image.
|
|
76
|
+
|
|
77
|
+
## 3. Deploy
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
kamal setup # first time
|
|
81
|
+
kamal deploy # subsequent
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Mid-deploy safety is built in: a turn interrupted by the SIGTERM of a rolling
|
|
85
|
+
deploy re-enqueues and resumes (chaos-gated). Recovery latency after a hard
|
|
86
|
+
crash ≈ `SolidQueue.process_alive_threshold` + the rescuer's 30s cadence.
|
|
87
|
+
|
|
88
|
+
## 3½. Template apps deploy stock
|
|
89
|
+
|
|
90
|
+
An app generated from `templates/*.rb` needs no deploy surgery: Rails 8.1's
|
|
91
|
+
generated `config/deploy.yml` already runs Solid Queue inside Puma for the
|
|
92
|
+
single-server SQLite shape (`SOLID_QUEUE_IN_PUMA: true` — that satisfies "the
|
|
93
|
+
worker must run"), the template adds `ANTHROPIC_API_KEY` to Kamal's secret
|
|
94
|
+
list and `.kamal/secrets`, and `config/recurring.yml` already carries the
|
|
95
|
+
rescuer. Set the two secrets in your shell, point `deploy.yml` at your server
|
|
96
|
+
and registry, `kamal setup`. Scale beyond one box by moving to the dedicated
|
|
97
|
+
worker role in §1 and a real Postgres in §2.
|
|
98
|
+
|
|
99
|
+
## 4. Cost
|
|
100
|
+
|
|
101
|
+
The whole thing runs on one VPS + your model spend. The engine authenticates
|
|
102
|
+
with an API key for whatever provider you configure through RubyLLM. Your data
|
|
103
|
+
and the ledger never leave your own Postgres, and there is no Silas platform
|
|
104
|
+
bill or per-run metering.
|
|
105
|
+
|
|
106
|
+
## 5. Local development note (macOS + Postgres)
|
|
107
|
+
|
|
108
|
+
Solid Queue forks workers, and on macOS the pg gem needs
|
|
109
|
+
`PGGSSENCMODE=disable OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES` in the
|
|
110
|
+
environment or forked workers crash inside libpq. SQLite development needs
|
|
111
|
+
nothing.
|
data/README.md
CHANGED
|
@@ -1,295 +1,126 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/danielstpaul/silas/main/brand/silas-wordmark.svg">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/danielstpaul/silas/main/brand/silas-wordmark-light.svg" alt="silas" width="150">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
2
7
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
the durable stack is already booted inside your app. The only new surface is the
|
|
9
|
-
`app/agent/` directory below.
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="https://rubygems.org/gems/silas"><img src="https://img.shields.io/gem/v/silas" alt="Gem"></a>
|
|
10
|
+
<a href="https://github.com/danielstpaul/silas/actions/workflows/ci.yml"><img src="https://github.com/danielstpaul/silas/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
11
|
+
<a href="https://github.com/danielstpaul/silas/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
|
|
12
|
+
</p>
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
Silas is a Rails-native framework for durable AI agents. An agent's
|
|
15
|
+
capabilities live as plain files in conventional locations inside the app you
|
|
16
|
+
already run — easy to inspect, extend, and operate — and its tool effects
|
|
17
|
+
land **exactly once**, even through a crash.
|
|
18
|
+
|
|
19
|
+
## The filesystem is the authoring interface
|
|
20
|
+
|
|
21
|
+
A typical Silas agent:
|
|
19
22
|
|
|
20
23
|
```
|
|
21
24
|
app/agent/
|
|
22
|
-
instructions.md # the persona
|
|
23
|
-
agent.yml #
|
|
24
|
-
tools/ # one file per tool; identity =
|
|
25
|
-
issue_refund.rb # keyword signature = the schema the model sees
|
|
25
|
+
instructions.md # the persona
|
|
26
|
+
agent.yml # model + limits — data only
|
|
27
|
+
tools/ # one file per tool; filename = identity, keywords = schema
|
|
26
28
|
skills/ # markdown playbooks, loaded on demand
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## Quickstart
|
|
31
|
-
|
|
32
|
-
```sh
|
|
33
|
-
bundle add silas
|
|
34
|
-
bin/rails generate silas:install
|
|
35
|
-
bin/rails db:migrate
|
|
36
|
-
bin/rails silas:doctor # key · queue adapter · model · migrations · tools · rescuer · cable · auth
|
|
29
|
+
schedules/ # cron frontmatter -> recurring turns
|
|
30
|
+
channels/ # slack.rb, email.rb — transports bound to the loop
|
|
31
|
+
connections/ # remote MCP servers as <name>.yml
|
|
37
32
|
```
|
|
38
33
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
transactional! # DB-only side effects -> exactly-once, guaranteed
|
|
34
|
+
Documentation lives at
|
|
35
|
+
**[danielstpaul.github.io/silas](https://danielstpaul.github.io/silas)** —
|
|
36
|
+
start with the [tutorial](https://danielstpaul.github.io/silas/tutorial). The
|
|
37
|
+
same docs ship inside the gem (`bundle show silas`), and the installer writes
|
|
38
|
+
a Claude Code skill so coding agents working in your app know the conventions.
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
Refund.create!(order_id:, amount:)
|
|
48
|
-
{ refunded: order_id }
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
```
|
|
40
|
+
## Quick start
|
|
52
41
|
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
session.pending_approvals.first.approve!(by: "daniel")
|
|
56
|
-
session.continue(input: "Now email the customer.")
|
|
42
|
+
```bash
|
|
43
|
+
rails new my-agent -m https://raw.githubusercontent.com/danielstpaul/silas/main/templates/desk.rb
|
|
57
44
|
```
|
|
58
45
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
46
|
+
This creates a new agent app with Solid Queue wired, a starter refund-desk
|
|
47
|
+
agent installed, and a keyless demo — the first `cd my-agent && bin/dev`
|
|
48
|
+
needs no API key. More starting shapes live in
|
|
49
|
+
[templates/](https://github.com/danielstpaul/silas/tree/main/templates) —
|
|
50
|
+
swap `desk.rb` for `analyst.rb` to start from a scheduled reporting agent
|
|
51
|
+
instead.
|
|
62
52
|
|
|
63
|
-
|
|
64
|
-
$ bin/rails silas:chat
|
|
65
|
-
you> Refund order 42, £12.50
|
|
66
|
-
✓ lookup_order(order_id: 42)
|
|
67
|
-
⏸ issue_refund(order_id: 42, amount: 1250) — awaiting approval
|
|
53
|
+
To add Silas to an existing app:
|
|
68
54
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
```bash
|
|
56
|
+
bundle add silas
|
|
57
|
+
bin/rails generate silas:install
|
|
58
|
+
bin/rails db:migrate
|
|
72
59
|
```
|
|
73
60
|
|
|
74
|
-
`
|
|
61
|
+
`bin/rails silas:doctor` verifies the whole setup.
|
|
75
62
|
|
|
76
|
-
##
|
|
63
|
+
## A minimal example
|
|
77
64
|
|
|
78
|
-
|
|
79
|
-
hundreds of times per release (results in `chaos_host/results/`):
|
|
65
|
+
Replace `app/agent/instructions.md` with your agent's persona:
|
|
80
66
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
ledger row commit or roll back together. Zero duplicates across every chaos run.
|
|
86
|
-
- **Other tools are at-least-once within one step** — and when a crash makes an
|
|
87
|
-
execution ambiguous, the default `at_most_once!` policy **parks the run for a
|
|
88
|
-
human verdict** instead of guessing (`idempotent!` opts into automatic re-runs).
|
|
89
|
-
- **Approvals park at zero compute** — the job exits; approving enqueues a fresh
|
|
90
|
-
one that replays completed work from rows, never re-calling the model or
|
|
91
|
-
re-running tools. Parks expire (default 7 days) rather than ghosting forever.
|
|
92
|
-
- **Transient model errors retry from the checkpoint.** A rate limit,
|
|
93
|
-
overload, or timeout backs off and retries the job — and the continuation
|
|
94
|
-
resumes from the last completed step, never re-running completed work.
|
|
95
|
-
Exhausted retries and permanent rejections (bad key, bad request) expire
|
|
96
|
-
pending approvals and fail the turn loudly. **A turn can never sit in
|
|
97
|
-
`running` forever**: the rescuer also fails turns stranded by a loop job
|
|
98
|
-
that died outside the retry list.
|
|
99
|
-
- **The rescuer is part of the contract.** Solid Queue marks a dead worker's
|
|
100
|
-
jobs failed and nothing retries them; the installer wires
|
|
101
|
-
`Silas::DeadJobRescuerJob` as a recurring task (every 30s). Recovery time ≈
|
|
102
|
-
`SolidQueue.process_alive_threshold` + that cadence. Do not remove it.
|
|
103
|
-
- **Deploys can't corrupt a run**: instructions are snapshotted per turn, and a
|
|
104
|
-
deploy that changes tools/skills mid-turn fails the turn loudly
|
|
105
|
-
(`NondeterminismError`) instead of resuming into a different agent.
|
|
106
|
-
|
|
107
|
-
## Adapter
|
|
108
|
-
|
|
109
|
-
Inference is one pluggable seam (`config.adapter`): `:ruby_llm` — API-key auth
|
|
110
|
-
via [RubyLLM](https://rubyllm.com), any provider it supports — is the default
|
|
111
|
-
and the production path. Compose resilience via `config.around_model_call`, or
|
|
112
|
-
swap in any object responding to `#execute_step` (the eval harness and the
|
|
113
|
-
chaos tests do exactly that).
|
|
114
|
-
|
|
115
|
-
> The experimental `:agent_sdk` adapter (a `claude -p` subprocess) was removed
|
|
116
|
-
> in 0.2: its subscription-auth rationale was structurally unreachable, and it
|
|
117
|
-
> carried weaker guarantees than `:ruby_llm` on every axis. Its in-process MCP
|
|
118
|
-
> server survives and returns as a first-class *mount your tools as MCP*
|
|
119
|
-
> feature.
|
|
120
|
-
|
|
121
|
-
## Sandbox: run untrusted code with hermetic
|
|
67
|
+
```markdown
|
|
68
|
+
You are the refund desk. Look orders up before promising anything, and never
|
|
69
|
+
quote an amount a tool didn't return.
|
|
70
|
+
```
|
|
122
71
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
honest-but-interim). For real isolation, the companion gem
|
|
126
|
-
[**hermetic**](https://github.com/danielstpaul/hermetic) drops straight in:
|
|
72
|
+
Create a tool at `app/agent/tools/issue_refund.rb` — the keyword signature
|
|
73
|
+
*is* the schema the model sees:
|
|
127
74
|
|
|
128
75
|
```ruby
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
#
|
|
76
|
+
class Agent::Tools::IssueRefund < Silas::Tool
|
|
77
|
+
description "Refund part or all of an order."
|
|
78
|
+
approval ->(session:, input:) { input[:amount_pence] > 2_500 ? :user_approval : :approved }
|
|
79
|
+
transactional! # DB effect + ledger commit atomically -> exactly-once
|
|
80
|
+
|
|
81
|
+
def call(number:, amount_pence:, reason:)
|
|
82
|
+
order = Order.find_by!(number: number)
|
|
83
|
+
refund = order.refunds.create!(amount_pence:, reason:)
|
|
84
|
+
{ refunded_pence: refund.amount_pence, order: order.number }
|
|
85
|
+
end
|
|
133
86
|
end
|
|
134
87
|
```
|
|
135
88
|
|
|
136
|
-
|
|
137
|
-
`run_code` tool is advertised to the model automatically (`at_most_once!` — an
|
|
138
|
-
exec is an external effect). Two properties carry through the seam:
|
|
139
|
-
|
|
140
|
-
- **The trust axis is visible**: every hermetic backend exposes `trust`
|
|
141
|
-
(`:vendor`/`:remote`/`:vm`/`:host`) and `off_host?`, so you can refuse to run
|
|
142
|
-
untrusted code on the box that holds your `RAILS_MASTER_KEY` — pair any local
|
|
143
|
-
backend with `executor:` to push execution to a dedicated sandbox host.
|
|
144
|
-
- **The ledger guard is auto-armed**: configuring a hermetic backend loads its
|
|
145
|
-
Silas shim, so a sandbox exec attempted inside a ledger transaction fails loud
|
|
146
|
-
(sandbox-backed tools must be `at_most_once!`, never `transactional!`).
|
|
147
|
-
|
|
148
|
-
## Named agents: the staff pattern
|
|
149
|
-
|
|
150
|
-
One app can employ several agents, each with its own room:
|
|
151
|
-
|
|
152
|
-
```
|
|
153
|
-
app/agents/
|
|
154
|
-
reader/ # Silas.agent(:reader).start(input: "...")
|
|
155
|
-
instructions.md
|
|
156
|
-
agent.yml # model, limits — same keys as the root agent
|
|
157
|
-
tools/
|
|
158
|
-
skills/
|
|
159
|
-
clerk/
|
|
160
|
-
...
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Sessions are stamped with the agent's name; every turn — including crash
|
|
164
|
-
resumes — runs under that agent's own tools, skills, instructions, and
|
|
165
|
-
definitions digest. The inbox filters by agent; `bin/rails silas:chat
|
|
166
|
-
AGENT=clerk` chats with one staff member. The root `app/agent/` remains the
|
|
167
|
-
default agent, unchanged. (Subagents stay a root-agent delegation feature;
|
|
168
|
-
scope switching is execution-isolated, so concurrent jobs running different
|
|
169
|
-
agents never cross wires.)
|
|
170
|
-
|
|
171
|
-
## Memory & handoffs
|
|
172
|
-
|
|
173
|
-
Silas memory is **graph-shaped, not a graph database**: facts as
|
|
174
|
-
`subject · attribute · content` triples with provenance and supersession
|
|
175
|
-
("author:jane · report_format: prefers CSV" — a new value retires the old).
|
|
176
|
-
The `remember` tool is **approval-gated by default** — the memory card parks
|
|
177
|
-
in your inbox before anything persists; `recall` digs deeper than the few
|
|
178
|
-
recent memories injected into each turn. Private per agent, or `shared: true`
|
|
179
|
-
for the whole staff. Your *domain* data does not belong here — it belongs in
|
|
180
|
-
your own tables, which your tools already read; memory is for the fuzzy
|
|
181
|
-
residue with no natural home.
|
|
182
|
-
|
|
183
|
-
Staff compose through **handoffs, not conversations**: `handoff` files a
|
|
184
|
-
self-contained brief that starts a linked session for another named agent
|
|
185
|
-
(async, or `await: true` for an answer), exactly-once-guarded, cycle-checked.
|
|
186
|
-
Two models chatting freely is a cost and audit hazard — deliberately
|
|
187
|
-
unblessed.
|
|
188
|
-
|
|
189
|
-
## Triggers
|
|
89
|
+
Restart, then talk to it:
|
|
190
90
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
- **`schedules/`** — `app/agent/schedules/*.md` (cron frontmatter, body = the turn
|
|
194
|
-
input) or `*.rb` handlers. `bin/rails silas:schedules` compiles them into
|
|
195
|
-
Solid Queue recurring tasks. A scheduled run is a normal durable turn. Named
|
|
196
|
-
agents own their cron the same way they own tools:
|
|
197
|
-
`app/agents/analyst/schedules/monday_kpis.md` ticks start the analyst, not
|
|
198
|
-
the root agent.
|
|
199
|
-
- **`channels/`** — `app/agent/channels/*.rb` bind email (Action Mailbox) and
|
|
200
|
-
Slack to the loop. A new thread starts a session, a reply continues it, and
|
|
201
|
-
approvals render as Slack buttons / signed email links that call the same
|
|
202
|
-
`approve!`/`decline!`. Outbound delivery is idempotent and off the durable loop.
|
|
203
|
-
`bin/rails g silas:channel whatsapp` scaffolds any other transport — a
|
|
204
|
-
signature-verifying webhook and the outbound half, wired together. See
|
|
205
|
-
[docs/channels.md](https://github.com/danielstpaul/silas/blob/main/docs/channels.md).
|
|
206
|
-
|
|
207
|
-
## Streaming
|
|
208
|
-
|
|
209
|
-
Turns stream. The `:ruby_llm` adapter emits text deltas as the model responds:
|
|
210
|
-
`bin/rails silas:chat` prints tokens as they arrive, and the inbox trace
|
|
211
|
-
renders them live over Turbo (coalesced to ~10Hz). Deltas are decoration over
|
|
212
|
-
the durable rows — never persisted, never fed back to the model, and a
|
|
213
|
-
replayed step renders from its row with no deltas at all, so streaming adds
|
|
214
|
-
zero risk to the durability contract. Custom sinks subscribe to the
|
|
215
|
-
`"delta.silas"` notification (`{ session_id:, turn_id:, step_id:, step_index:,
|
|
216
|
-
text: }`, where `text` is the accumulated string so far — filter by ids;
|
|
217
|
-
notifications are process-global).
|
|
218
|
-
|
|
219
|
-
## Structured answers
|
|
220
|
-
|
|
221
|
-
Give the turn's final answer a schema in agent.yml and read it back as a Hash:
|
|
222
|
-
|
|
223
|
-
```yaml
|
|
224
|
-
final_answer:
|
|
225
|
-
type: object
|
|
226
|
-
properties:
|
|
227
|
-
verdict: { type: string }
|
|
228
|
-
amount_pence: { type: integer }
|
|
229
|
-
required: [verdict]
|
|
91
|
+
```bash
|
|
92
|
+
bin/rails silas:chat
|
|
230
93
|
```
|
|
231
94
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
loudly rather than resuming into a different contract.
|
|
95
|
+
Refunds over £25 hold for a person — in the operator inbox the gem mounts at
|
|
96
|
+
`/silas/inbox`, in Slack, or over the JSON API — and approving resumes the
|
|
97
|
+
turn exactly where it stopped, with exactly one refund row in your database.
|
|
98
|
+
How that's guaranteed (and verified with a `kill -9` chaos harness on every
|
|
99
|
+
release): [guarantees](https://danielstpaul.github.io/silas/guarantees).
|
|
238
100
|
|
|
239
|
-
##
|
|
101
|
+
## Status
|
|
240
102
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
103
|
+
Early (0.5.x) and moving fast. Requires Rails >= 8.1 (Active Job
|
|
104
|
+
Continuations) and Solid Queue >= 1.2; any model provider
|
|
105
|
+
[RubyLLM](https://rubyllm.com) supports.
|
|
244
106
|
|
|
245
|
-
|
|
246
|
-
curl -X POST .../silas/api/v1/sessions -d "input=Refund order 42, £12.50"
|
|
247
|
-
curl .../silas/api/v1/sessions/1?trace=1 # turns + steps + tool calls
|
|
248
|
-
curl .../silas/api/v1/sessions/1/approvals # what's parked
|
|
249
|
-
curl -X POST .../silas/api/v1/approvals/7/approve # the same approve! as the inbox
|
|
250
|
-
curl -X POST .../silas/api/v1/sessions/1/turns -d "input=Now email them" # 409 if busy
|
|
251
|
-
curl -X POST .../silas/api/v1/turns/9/cancel
|
|
252
|
-
curl -N .../silas/api/v1/sessions/1/stream # server-sent events
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
The stream is SSE at **row granularity** — turn / completed-step / invocation
|
|
256
|
-
changes, at-least-once with `Last-Event-ID` resume (ids are epoch-ms
|
|
257
|
-
watermarks; `?poll=1` returns the backlog and closes, curl-friendly; streams
|
|
258
|
-
close themselves after `api_stream_max_duration` and clients reconnect).
|
|
259
|
-
Per-token streaming is deliberately the browser/Turbo feature — deltas live in
|
|
260
|
-
the worker process, and the gem requires no cross-process bus.
|
|
261
|
-
|
|
262
|
-
## The inbox
|
|
107
|
+
## Community
|
|
263
108
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
the browser — same durable loop, no separate surface), a live step-trace that
|
|
267
|
-
streams tokens over Turbo Streams as the agent runs, approval cards whose
|
|
268
|
-
Approve/Decline buttons call the exact same `approve!`/`decline!` as Slack and
|
|
269
|
-
email, a full **audit trail** (every tool call's arguments and its result or
|
|
270
|
-
recorded error; who approved; who declined and why), **cancel** on active
|
|
271
|
-
turns (honored at the next step boundary), and per-session token/cost
|
|
272
|
-
accounting. It's **deny-by-default** — invisible until you wire auth:
|
|
109
|
+
Questions, ideas, and bug reports →
|
|
110
|
+
[issues](https://github.com/danielstpaul/silas/issues).
|
|
273
111
|
|
|
274
|
-
|
|
275
|
-
Silas.configure do |c|
|
|
276
|
-
# Devise-compatible: the lambda DENIES by rendering; passes by not rendering.
|
|
277
|
-
c.inbox_auth = ->(controller) { controller.head :not_found unless controller.current_user&.admin? }
|
|
278
|
-
# c.inbox_public_read = true # public read-only demo; approve/decline stay gated
|
|
279
|
-
# c.model_prices["your-model"] = { in: 300, out: 1500 } # microcents / 1k tokens
|
|
280
|
-
end
|
|
281
|
-
```
|
|
112
|
+
## Contributing
|
|
282
113
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
114
|
+
See [CONTRIBUTING.md](https://github.com/danielstpaul/silas/blob/main/CONTRIBUTING.md)
|
|
115
|
+
— including the short list of things Silas deliberately doesn't do, and the
|
|
116
|
+
chaos gate that protects the durability contract.
|
|
286
117
|
|
|
287
|
-
##
|
|
118
|
+
## Security
|
|
288
119
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
120
|
+
Please report vulnerabilities privately via
|
|
121
|
+
[GitHub's vulnerability reporting](https://github.com/danielstpaul/silas/security/advisories/new)
|
|
122
|
+
— see [SECURITY.md](https://github.com/danielstpaul/silas/blob/main/SECURITY.md).
|
|
292
123
|
|
|
293
124
|
## License
|
|
294
125
|
|
|
295
|
-
MIT.
|
|
126
|
+
[MIT](https://github.com/danielstpaul/silas/blob/main/LICENSE).
|