silas 0.4.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +211 -0
  3. data/DEPLOY.md +111 -0
  4. data/README.md +87 -250
  5. data/app/controllers/silas/api/v1/approvals_controller.rb +10 -0
  6. data/app/controllers/silas/inbox/invocations_controller.rb +8 -0
  7. data/app/helpers/silas/inbox/trace_helper.rb +20 -5
  8. data/app/jobs/silas/channel_delivery_job.rb +15 -0
  9. data/app/models/concerns/silas/inbox/broadcastable.rb +12 -0
  10. data/app/models/silas/compaction.rb +32 -0
  11. data/app/models/silas/tool_invocation.rb +29 -3
  12. data/app/views/layouts/silas/inbox.html.erb +89 -30
  13. data/app/views/silas/inbox/invocations/_approval_card.html.erb +36 -13
  14. data/app/views/silas/inbox/invocations/_invocation.html.erb +17 -5
  15. data/app/views/silas/inbox/sessions/_row.html.erb +14 -0
  16. data/app/views/silas/inbox/sessions/index.html.erb +16 -15
  17. data/app/views/silas/inbox/sessions/show.html.erb +9 -0
  18. data/config/routes.rb +2 -0
  19. data/db/migrate/20260725000002_create_silas_compactions.rb +26 -0
  20. data/docs/agents.md +81 -0
  21. data/docs/budgets.md +67 -0
  22. data/docs/cancellation.md +41 -0
  23. data/docs/channels.md +290 -0
  24. data/docs/configuration.md +106 -0
  25. data/docs/connections.md +55 -0
  26. data/docs/conventions.md +161 -0
  27. data/docs/evals.md +95 -0
  28. data/docs/guarantees.md +76 -0
  29. data/docs/inbox-and-api.md +84 -0
  30. data/docs/memory.md +35 -0
  31. data/docs/sandbox.md +44 -0
  32. data/docs/tools.md +77 -0
  33. data/docs/tutorial.md +268 -0
  34. data/docs/vs-eve.md +93 -0
  35. data/docs/why-silas.md +87 -0
  36. data/lib/generators/silas/channel/channel_generator.rb +72 -0
  37. data/lib/generators/silas/channel/templates/channel.rb.tt +48 -0
  38. data/lib/generators/silas/channel/templates/controller.rb.tt +66 -0
  39. data/lib/generators/silas/install/install_generator.rb +11 -1
  40. data/lib/generators/silas/install/templates/claude_skill.md +136 -0
  41. data/lib/generators/silas/install/templates/ruby_llm.rb +4 -1
  42. data/lib/silas/adapters/ruby_llm.rb +102 -46
  43. data/lib/silas/channel.rb +35 -0
  44. data/lib/silas/compactor.rb +178 -0
  45. data/lib/silas/configuration.rb +16 -0
  46. data/lib/silas/eval/dsl.rb +7 -2
  47. data/lib/silas/instrumentation.rb +7 -3
  48. data/lib/silas/ledger.rb +2 -2
  49. data/lib/silas/log_subscriber.rb +5 -0
  50. data/lib/silas/message_builder.rb +19 -0
  51. data/lib/silas/registry.rb +5 -2
  52. data/lib/silas/schedule.rb +44 -15
  53. data/lib/silas/slack.rb +8 -5
  54. data/lib/silas/step_runner.rb +5 -0
  55. data/lib/silas/tools/ask_question.rb +26 -0
  56. data/lib/silas/version.rb +1 -1
  57. data/lib/silas/webhook.rb +47 -0
  58. data/lib/silas.rb +3 -0
  59. metadata +30 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9abd9603ae899da9a789ded30f092f11f1881ddc9abd2f0edf2638421f469abb
4
- data.tar.gz: 16a684a62b54346ee51d4e7b9f6cb92ec19c2d95e7027667b432044d9a1e7f71
3
+ metadata.gz: fd68f6d368466b20fbb35f49628f166ac61dd60ace3a8cbd20bbc6e316c1d346
4
+ data.tar.gz: 94d2a902cd7f364aa33200b1ea1f98446dabee38d9e9f7431df9cd96d1fc7f41
5
5
  SHA512:
6
- metadata.gz: 9518d703a421ed47d1c59e9cb74cdbd46686b8a783a1f5aea8f90f5911e0e27f5ccc5fc86da22c8ebe870a3d811b7de28e5abfc2ff837ab7dbc47722d13e3dcc
7
- data.tar.gz: 954efe8fac65a18a0d936bafda946bcca96e8dda35201d06637d8f6ee5a8ca425ecd50ab322d2f137e7c0d03b2e97321db7d5cedfc5ac7d0a4b016ddf3184b63
6
+ metadata.gz: c386608b076c2c3dfb23e369c819a7d7b1c4fc6122f0011ea80880a46b158b25549a75388a17de250b200a60e0ada311a73df28b47c67e4007cc9e2ebef8bf3f
7
+ data.tar.gz: 4216b83c6925605076cb3742286699673e2a18dfd9eab3a3cc6e21479ee64e17de714737e5764acce0f147ca0023ced60ed0864b0836850c771dbab88ea84100
data/CHANGELOG.md CHANGED
@@ -1,5 +1,216 @@
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
+
104
+ ## 0.5.0 (2026-07-26)
105
+
106
+ Two new loop primitives (replay-safe compaction, ask_question), a whole-channel
107
+ generator, the adapter rebound onto RubyLLM's public single-turn seam, and
108
+ per-agent schedules. Chaos-gated: **295 kill/deploy cycles across both stores —
109
+ zero duplicate side effects, byte-identical replay** — including a new compact
110
+ mode that kills mid-summarisation and asserts the compaction claim is
111
+ exactly-once and the rebuilt provider messages are byte-identical
112
+ (`chaos_host/RESULTS.md`).
113
+
114
+ ### Added
115
+
116
+ - **Per-agent schedules.** Named agents own their cron the way they own tools
117
+ and skills: `app/agents/analyst/schedules/monday_kpis.md` is discovered as
118
+ `agents/analyst/monday_kpis`, compiled by `silas:schedules` under a
119
+ collision-free recurring key, and its ticks start **the analyst** — a staff
120
+ member's schedule never wakes the root agent. `.rb` handlers resolve under
121
+ the agent's namespace (`Agents::Analyst::Schedules::MondayKpis`).
122
+
123
+ - **`ask_question` — the agent can park to ask a human something.**
124
+ Information, not permission: the model calls the new builtin with a
125
+ question, the turn parks at zero compute through the same machinery as
126
+ approvals (TTL, channel ping, resume gate), and the operator's free-text
127
+ reply becomes the tool result the model resumes with
128
+ (`{"answer" => "..."}`). Answer from the inbox (a question card with a text
129
+ box replaces approve/decline) or the API
130
+ (`POST /silas/api/v1/approvals/:id/answer {text:}`); `decline!` remains the
131
+ refusal path, and an unanswered question expires as
132
+ `{"answer" => nil, "note" => "question expired unanswered"}`. Channels are
133
+ pinged only if they implement `deliver_question` — buttons are the wrong UI
134
+ for free text, so transports without it simply leave the question in the
135
+ inbox. Disable with `config.ask_question = false`.
136
+
137
+ **Upgrade note:** adding a builtin changes the definitions digest, so turns
138
+ parked across the upgrade fail loudly on resume (the nondeterminism guard
139
+ working as designed). Settle parked turns before upgrading, or set
140
+ `config.ask_question = false` to keep the old digest.
141
+
142
+ - **Context compaction that survives replay.** Long sessions used to grow
143
+ until the provider rejected the prompt and the turn failed. Now, when the
144
+ measured context passes `config.compact_at` (default 0.9 of the model's
145
+ registry context window; set an Integer for an absolute token threshold, or
146
+ nil to disable), Silas summarises all prior turns into a `silas_compactions`
147
+ row and the conversation continues — the current turn is never compacted.
148
+
149
+ The design constraint is the durability contract: replayed executions must
150
+ see byte-identical message arrays, so a summary can never be computed at
151
+ build time. Compaction is an *effect*, made exactly-once the way tool
152
+ effects are — claimed compare-and-swap (unique index per session + span),
153
+ generated once, then read deterministically from the row forever. A crash
154
+ mid-summary leaves a pending row the resume finishes; a crash mid-step
155
+ replays against the identical compacted history. New `compact.silas`
156
+ instrumentation event (duration = the summarisation call). Chaos-gated with
157
+ a dedicated mode: kill -9 during the compacting turn, including
158
+ mid-summarisation.
159
+
160
+ ### Changed
161
+
162
+ - **The `:ruby_llm` adapter no longer fights the library.** `Chat#complete`
163
+ runs RubyLLM's whole agentic loop — model, execute tools, feed results back,
164
+ model again — but Silas needs a single move, because the step boundary *is*
165
+ the durability boundary. It used to get one by registering tool proxies that
166
+ threw `RubyLLM::Tool::Halt` to abort the loop from the inside.
167
+
168
+ Chat is now used as the builder it is (it owns model resolution, schema
169
+ normalisation, system instructions and message construction) and execution
170
+ drops one layer to `RubyLLM::Provider#complete` — the same call Chat makes
171
+ internally for a single turn. Entirely public API, and the adapter got
172
+ smaller: no `Tool::Halt`, no hunting back through `chat.messages` for the
173
+ assistant reply, and the `before_message` streaming-timing oddity is gone in
174
+ favour of an event Silas emits itself.
175
+
176
+ **This removes Silas's exposure to the largest RubyLLM 2.0 breaking change.**
177
+ 2.0 deletes `Tool::Halt` precisely because the loop became caller-controlled;
178
+ Silas no longer needs it either way. The adapter also now calls `with_tools`
179
+ (2.0 drops the singular `with_tool`) and its schema proxy answers to both
180
+ `params_schema` and `parameters_schema` (2.0 renames it), so the tool path is
181
+ version-agnostic today. No behaviour change for users.
182
+
183
+ ### Added
184
+
185
+ - **`rails g silas:channel <name>`** — scaffolds a whole channel, not half of
186
+ one. Channels were reachable before (`Channel.dispatch` is a ~50-line seam)
187
+ but the engine ships webhook routes for Slack only, so any other transport
188
+ meant hand-rolling a controller, a route, and signature verification with no
189
+ documented contract. The generator writes the outbound `Channel` subclass,
190
+ a signature-verifying inbound controller, and the route that joins them —
191
+ with the security decisions already made: verify before anything else, sign
192
+ over the raw body, fail closed on a missing secret, and send approvals to an
193
+ operator rather than to whoever started the session.
194
+ - **`Silas::Webhook.verify_hmac`** — the parts of webhook verification that are
195
+ identical for every vendor (constant-time comparison, replay window,
196
+ fail-closed on a missing secret), with the vendor's shape (`payload`,
197
+ `prefix`, `digest`) supplied by the caller. `Silas::Slack.verify_signature`
198
+ now delegates to it and keeps its exact v0 scheme.
199
+ - **`Silas::Channel.approval_url(invocation, action)`** — a signed, expiring
200
+ one-click approve/decline link for *any* transport, built from the engine's
201
+ route set and the discovered mount point, so it works from a delivery job
202
+ with no routing scope. Raises with the fix when no host is configured rather
203
+ than minting a dead link.
204
+ - `docs/channels.md`: the inbound/outbound contract, a per-vendor signature
205
+ table, and a worked WhatsApp Cloud API example.
206
+
207
+ ### Removed
208
+
209
+ - `demo/refund-desk` and `demo/churn-desk`. Both were copy-paste kits whose
210
+ READMEs instructed deleting a file the generated eval still asserted on —
211
+ broken on arrival. `examples/playground` is the example; `docs/why-silas.md`
212
+ and `docs/vs-eve.md` now point at it.
213
+
3
214
  ## 0.4.0
4
215
 
5
216
  The architecture-and-hardening release: one shipped feature that had never
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.