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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f722788fa436905b6ea94ee9666ca71013fa6eace683f7d3dc8eec4f257fbe2
4
- data.tar.gz: 85d3ae116127d4b91207526d7936440092250854380938a94eba177cd26f3eeb
3
+ metadata.gz: fd68f6d368466b20fbb35f49628f166ac61dd60ace3a8cbd20bbc6e316c1d346
4
+ data.tar.gz: 94d2a902cd7f364aa33200b1ea1f98446dabee38d9e9f7431df9cd96d1fc7f41
5
5
  SHA512:
6
- metadata.gz: 645b843c5dbde892fa0725c1f33302bf39b3b10eef43ff14b0828efeb0420ead72d82e34b9bb3828cf99aa8f880db4f6017f2913023884a679385ca68a21ae41
7
- data.tar.gz: 2b5b8df36d8747fde43b1eccffb466f17a3a9ef430ebc77b055b36c73ff987c4e07c66eb14ef2265d34b37b1ff824cf4681737a2bca1333e4e7dad88c8db142b
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
- # Your Rails app is already an agent runtime.
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
- **Silas** turns the Rails app you already run into a durable AI-agent runtime.
4
- Active Job Continuations, Solid Queue, and one Postgres ledger table make every
5
- turn survive `kill -9` and resume from the last completed step — with a live
6
- operator inbox at `/silas/inbox` and park-at-zero human-in-the-loop approvals
7
- holding the big levers. No new service, no managed platform, no per-run meter:
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
- Honestly early and honestly narrow: **v0.1, one maintainer, zero external
12
- users**, durability proven by an in-repo `kill -9` chaos harness (100/100, zero
13
- duplicate effects, byte-identical replay), and scoped to **trusted code you write
14
- yourself** by default for untrusted or model-generated code, drop in the
15
- companion gem [hermetic](https://github.com/danielstpaul/hermetic) (gVisor /
16
- Firecracker / hosted sandboxes behind one call, see below). The full pitch
17
- and the honest caveats: [Why Silas](docs/why-silas.md) ·
18
- [Silas vs eve](docs/vs-eve.md).
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 (ERB, snapshotted once per turn)
23
- agent.yml # data-only config: model, limits
24
- tools/ # one file per tool; identity = filename
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
- triage.md # description: frontmatter is the routing hint
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
- ```ruby
40
- class Agent::Tools::IssueRefund < Silas::Tool
41
- description "Refund an order."
42
- param :amount, :integer, desc: "Pence"
43
- approval :always # parks the run; a human approves from your app
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
- def call(order_id:, amount:)
47
- Refund.create!(order_id:, amount:)
48
- { refunded: order_id }
49
- end
50
- end
51
- ```
40
+ ## Quick start
52
41
 
53
- ```ruby
54
- session = Silas.agent.start(input: "Refund order 42, £12.50")
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
- Or talk to it from the terminal the REPL runs *inside your app*, so tools hit
60
- your real dev database, and parked approvals prompt inline (the same
61
- `approve!`/`decline!` as the inbox and Slack):
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
- approval needed — issue_refund(order_id: 42, amount: 1250)
70
- approve? [y]es / [d]ecline / [s]kip> y
71
- agent> Done — £12.50 refunded on order 42.
55
+ ```bash
56
+ bundle add silas
57
+ bin/rails generate silas:install
58
+ bin/rails db:migrate
72
59
  ```
73
60
 
74
- `SESSION=id` resumes an existing session.
61
+ `bin/rails silas:doctor` verifies the whole setup.
75
62
 
76
- ## The durability contract (what's actually guaranteed)
63
+ ## A minimal example
77
64
 
78
- Verified by `chaos_host/bin/chaos` the harness that kill -9s a live agent
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
- - **A turn survives hard process death** (worker kill -9, whole-tree kill -9,
82
- SIGTERM deploys) and resumes from the last completed step: 100% completion,
83
- byte-identical transcripts, on SQLite and Postgres.
84
- - **`transactional!` tools execute exactly once.** The tool's DB writes and the
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
- The sandbox is a second pluggable seam (`config.sandbox`). Built-in adapters are
124
- `:none` (default code execution off) and `:docker` (hardened container,
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
- # Gemfile: gem "hermetic" (zero runtime deps)
130
- Silas.configure do |c|
131
- c.sandbox = Hermetic.gvisor(image: "python:3.12-slim") # or .docker /
132
- # .firecracker(kernel:, rootfs:) / .hosted(:e2b, api_key:) # pick your strength
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
- That's the whole integration. When a sandbox is configured and enabled, the
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
- An agent is reached by more than a method call:
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
- `Turn#answer_data` returns the parsed payload (`answer_text` stays for prose
233
- agents); the HTTP API carries it as `answer_data`, evals get
234
- `assert_answer_data(key: :verdict, value: "approve")`. Rendered through
235
- RubyLLM's `with_schema`, so each provider's native structured-output mode is
236
- used. The schema is model-visible state — changing it mid-turn fails the turn
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
- ## The HTTP API
101
+ ## Status
240
102
 
241
- Everything the operator surface can do, over JSON mounted with the engine at
242
- `/silas/api/v1`, **deny-by-default** like the inbox (wire `config.api_auth`;
243
- `config.api_actor` names the identity recorded on approvals):
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
- ```sh
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
- Mount the engine (the generator does this) and a live inbox appears at
265
- `/silas/inbox`: a session list, **web chat** (start a session or reply from
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
- ```ruby
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
- Turbo streaming activates automatically when the host has `turbo-rails` (every
284
- default Rails app does); without it the trace falls back to a polling refresh.
285
- The gem itself takes no turbo dependency.
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
- ## Requirements
118
+ ## Security
288
119
 
289
- Rails >= 8.1 (Active Job Continuations) and Solid Queue >= 1.2 for the
290
- durability contract. macOS dev note: Solid Queue forks + pg need
291
- `PGGSSENCMODE=disable OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES`.
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).