eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– Bot recipes: behaviour as data
2
3A **recipe** is a bot's behaviour written as data β€” an ordered list of
4trigger→action rules from a small, bounded palette — executed by the Go bot
5runtime (`lchat bot run`, `internal/bot/recipe.go`). It's the concrete form of
6the decision in `docs/bots.md` that the Go runtime should be *recipe-shaped
7from day one*: the engine executes config, so declarative bots (and eventually
8the admin console's "describe a bot" front-end) need no code and no second
9rewrite.
10
11Recipes have a **sibling schema, deliberately not unified**:
12`docs/conversation-scripts.md` β€” Eleven's onboarding conversations as data,
13interpreted *client-side* per platform. Conversation scripts choreograph a UI
14the member is looking at; recipes run headless in a sidecar. They share a
15philosophy (flows as data, closed capability sets, fail-closed loading), not a
16language.
17
18## The palette is the capability boundary
19
20A recipe can only express what the schema names. That's the point: an action
21the palette doesn't have *cannot be written down*, so a recipe is auditable by
22reading it, and the plain-English builder planned in `docs/bots.md`
23Β§ "Creating bots in plain English" can only ever emit things an operator could
24have typed. Out-of-palette ideas ("delete messages", "email people") have no
25spelling. Growth happens by adding verbs to the schema β€” a code change, a
26review β€” never by a recipe smuggling one in.
27
28Loading **fails closed**: a recipe that doesn't validate stops the bot with an
29error. It never falls back to different behaviour the operator didn't write.
30
31## Schema v1
32
33A JSON file next to the bot's session file β€” `<session>.recipe.json`, the same
34sibling convention as a changelog rule's `.state.json`. No recipe file means the
35**default recipe**: answer the deterministic command floor when addressed
36(exactly the keyless Node sidecar's behaviour).
37
38```json
39{
40 "rules": [
41 { "on": "message", "match": "any", "regex": "(?i)\\bguys\\b",
42 "action": "reply", "text": "Hey {name} β€” how about \"folks\"?" },
43 { "on": "message", "action": "commands" },
44 { "on": "changelog", "action": "announce", "every": "5m" }
45 ]
46}
47```
48
49Rules are evaluated **in order; the first matching rule acts** and the rest
50are skipped.
51
52### Triggers (`on`)
53
54- **`message`** β€” an incoming chat message the bot can read. Refinements:
55 - `match` β€” who the rule listens to. `"trigger"` (the default) fires only
56 when the bot is *addressed*: a DM, or a group @mention. `"any"` fires on
57 every message β€” the loud **watcher** mode (below).
58 - `keyword` β€” case-insensitive substring gate on the text.
59 - `regex` β€” RE2 gate. Both set = both must match.
60- **`changelog`** β€” new entries in the instance's embedded changelog (the
61 the retired default Announcer's trigger β€” kept for custom deploy
62 watchers). `every` sets the poll interval (Go duration, default
63 `5m`). At most one changelog rule per recipe β€” one announce memory per bot.
64
65### Actions (`action`)
66
67- **`commands`** β€” answer the deterministic command floor (`/help`, `/theme`,
68 `/dark`, …, `internal/bot/commands.go`). A non-command falls through to
69 later rules; if nothing else acts and the bot was addressed, it answers with
70 the "try /help" hint. Message rules only.
71- **`reply`** β€” post `text` in the thread, with `{name}` replaced by the
72 sender's name. Deterministic, no model call β€” per `docs/bots.md`, a fixed
73 template is often the *best* form for a nudge. Message rules only.
74- **`model`** β€” phrase the reply with Claude (the runtime's model layer,
75 `internal/bot/brain.go`): builds context from thread history, calls the
76 Messages API with the layered system prompt + chip tools, posts the result.
77 Needs `ANTHROPIC_API_KEY` (the bot then badges `ai`); with no key it falls
78 through to later rules, so a recipe reads the same with or without a brain.
79 This is the one action that sends text to a provider β€” hence its place in a
80 watcher matters (below). Message rules only.
81- **`announce`** β€” post fresh changelog entries to every thread the bot is in
82 (the retired Announcer's full behaviour, including the silent first-run
83 baseline).
84 Changelog rules only.
85
86The **default recipe** (no file) is `commands` then `model`: answer a command
87when addressed, else let the brain reply β€” exactly the Node sidecar with and
88without a key.
89
90Planned, not in v1: a `schedule` trigger and a private-DM nudge variant. Each
91lands as a schema addition with its own review, per the boundary above.
92
93## Watchers are loud
94
95`match: "any"` is the **"reads every message in this group"** mode from
96`docs/bots.md` Β§ Consent β€” the exception, never the default. The engine
97surfaces it (`Recipe.Watches()`; `lchat bot run` logs *"watches every
98message"* at startup), and the consent story rides on it: adding a watcher to
99a group is the consent moment, and the admin console should show the flag
100wherever the bot appears. Detection stays in the sidecar β€” with a
101deterministic `reply` action, a watched message **never leaves the box at
102all**; with a `model` action, only *matches* ever reach the provider.
103
104## Execution
105
106`lchat bot run -base <url> a.session.json b.session.json …` loads each
107session's recipe (or the default), then runs one `bot.Engine` per identity:
108the live WebSocket runtime with the message rules folded into its Handler
109seam, plus one poller per changelog rule. One process, many bots, zero Node.
110
111A changelog recipe replaces the retired `lchat bot announce -poll` for long-running
112setups (`announce` remains for cron-style one-shots); both share the same
113`.state.json`, so switching modes never replays entries.
114