130 lines
8.5 KiB
Markdown
130 lines
8.5 KiB
Markdown
# Events: the shared contract
|
||
|
||
The Go server emits these over Server-Sent Events (SSE). The web UI turns them into Rive state machine inputs. Both sides build against this file; change it first, then the code.
|
||
|
||
Each SSE message: `event: <name>` and `data: <JSON>`. Every payload includes `list_id` and `ts` (RFC 3339).
|
||
|
||
| Event | Payload (besides list_id, ts) | Meaning |
|
||
|---|---|---|
|
||
| `list.started` | `items: [{item_id, text}]`, `requester` | Flock accepted a list |
|
||
| `item.triaged` | `item_id`, `mode` (solo/split/murder), `task_type`, `subtasks[]`, `reason`, `needs_approval` (bool) | Sentry made its call |
|
||
| `crow.flying` | `item_id`, `crow` | A crow (or Sentry, Elder) took off |
|
||
| `crow.landed` | `item_id`, `crow`, `ok` (bool), `tokens`, `error?` | A crow came back, or didn't |
|
||
| `item.answered` | `item_id`, `answer`, `crows[]`, `needs_approval` (bool) | Result ready |
|
||
| `verdict` | `item_id`, `target` (triage/output), `verdict` (approved/rejected/auto/timeout) | Judgment recorded |
|
||
| `trust.changed` | `holder` (crow name or "flock"), `task_type`, `from`, `to` (0 stranger, 1 regular, 2 trusted) | Level up or down |
|
||
| `item.skipped` | `item_id`, `reason` | Item dropped (rejected triage, budget, failure) |
|
||
| `requester.pranked` | `message`, `cooldown_s` | Abuse response |
|
||
| `list.finished` | `tokens_used`, `token_budget` | Done |
|
||
|
||
## HTTP API
|
||
|
||
| Method | Path | Body / query | Returns |
|
||
|---|---|---|---|
|
||
| POST | `/api/lists` | `{"text": "<todo list>", "requester": "<id>"}` | `{"list_id"}`, or 429 with `{"message","cooldown_s"}` when pranked |
|
||
| GET | `/api/events` | `?list_id=` | SSE stream |
|
||
| POST | `/api/verdicts` | `{"item_id", "target": "triage"\|"output", "approve": bool}` | 204 |
|
||
| GET | `/api/trust` | | ledger JSON |
|
||
|
||
Pending approvals that get no answer within 10 minutes count as rejected (`verdict: timeout`). Fail safe.
|
||
|
||
## Nest messages
|
||
|
||
A nest is a conversation. Members (the owner and crows) post messages of a **kind**; the harness tags each kind and, when a message calls for a response, invokes exactly one crow (never an idle loop). Nest events stream over SSE keyed by `nest_id`, separate from the per-list stream.
|
||
|
||
### Kinds
|
||
|
||
| Kind | Who posts it | What it does |
|
||
|---|---|---|
|
||
| `say` | any member | Ordinary talk. No response required. The default when nothing else fits. |
|
||
| `request` | any member | Asks a specific member (or the room) for work. Needs a claim from an addressed crow. |
|
||
| `claim` | a crow | Takes ownership of a `request`. solo/split: first claim wins. **conspiracy:** multiple claims are expected, each a distinct lens. |
|
||
| `deliver` | a crow | The result, addressed back to whoever requested. |
|
||
| `refer` | any member | Points to an artifact by name instead of pasting it. |
|
||
| `decline` | a crow | Refuses a request with a reason. Counts as responding. |
|
||
| `nudge` | harness only | A Jev verdict handed to one crow to proceed on a missed expectation. Not a chat message from a crow. |
|
||
| `flag` | Sentry only | A suspected contract breach. Quarantines the crow and opens an `ask_owner`. |
|
||
|
||
### Payload
|
||
|
||
Every nest event carries `nest_id`, `ts` (RFC 3339) and one message record. The record:
|
||
|
||
| Field | Type | Meaning |
|
||
|---|---|---|
|
||
| `msg_id` | string | `<nest_id>-<n>`, numbered from 1 in nest order. |
|
||
| `kind` | string | One of the seven kinds above. |
|
||
| `author` | string | `"owner"`, a crow name, `"harness"` (nudge) or `"Sentry"` (flag). |
|
||
| `text` | string | The message body. |
|
||
| `to` | string | The @-addressed member, or `""`. `deliver`/`claim` default to the requester they answer. |
|
||
| `refers_to` | string | The `msg_id` this responds to (claim/deliver/nudge point at their request/expectation), or `""`. |
|
||
| `tagged_by` | string | How `kind` was decided: `"jev"`, `"header"`, `"disagreement"` (Jev overrode the crow's header) or `"default"` (nothing fit, became `say`). |
|
||
|
||
### SSE event
|
||
|
||
| Event | Meaning |
|
||
|---|---|
|
||
| `nest.message` | A message was posted to the nest. Payload is the full record above. |
|
||
|
||
Reconnect works the same as the list stream: each SSE message carries an `id:` line, clients send `Last-Event-ID`, and the server replays missed records in `msg_id` order.
|
||
|
||
### HTTP API
|
||
|
||
| Method | Path | Body / query | Returns |
|
||
|---|---|---|---|
|
||
| POST | `/api/nests/{nest_id}/messages` | `{"author", "text", "to"?, "kind"? (crow header), "refers_to"?}` | 201 with the stored record (server-assigned `kind`, `tagged_by`, `msg_id`). 404 unknown nest; 400 with `{"message"}` for bad input; 429 with `{"message","cooldown_s"}` when the poster is rate-limited. |
|
||
| GET | `/api/events` | `?nest_id=<id>` (parallel to `?list_id=`) | SSE stream of `nest.message` events for that nest. |
|
||
|
||
### Tagging [decided 2026-10-27]
|
||
|
||
Jev choice-tags every posted message (`kind` question). Above its high band, Jev's tag stands (`tagged_by: "jev"`). If Jev is unsure, the crow's own `kind` header is used (`"header"`). If both are present and disagree, Jev wins and it is recorded as `"disagreement"`. If neither yields a kind, the message is `say` (`"default"`). Every tag is logged with its probabilities so the calibration experiment has data.
|
||
|
||
### Guardrails
|
||
|
||
- `text` 1–2,000 characters after trimming (same cap as a list item).
|
||
- `author` must be `"owner"` or a member of the nest; a non-member posts through the assistant or a member (room membership is a boundary).
|
||
- `kind`, when a crow supplies a header, must be one of the crow-postable kinds (`say`, `request`, `claim`, `deliver`, `refer`, `decline`). Crows may not post `nudge` or `flag`.
|
||
- Requests per minute and cooldown reuse the requester strikes in the ledger, keyed by `author`.
|
||
|
||
## Settled details
|
||
|
||
- `item_id` is `<list_id>-<n>`, numbered from 1 in list order.
|
||
- `POST /api/lists` returns 202. Bad input returns 400 with `{"message"}`.
|
||
- `POST /api/verdicts` returns 404 when nothing is waiting (including a second answer), 400 for a bad body. `approve` is required, so a missing value is never read as a no.
|
||
- When trust skips approval, the server still emits `verdict: "auto"`, so every target gets exactly one verdict.
|
||
- `crow.flying` and `crow.landed` fire for the Sentry and Elder too.
|
||
- `requester.pranked` goes to every list of that requester's still flying; if none, only the 429 is sent.
|
||
- Each SSE message carries an `id:` line, so clients can reconnect with `Last-Event-ID`. Heartbeat comment every 15 seconds.
|
||
- The server listens on localhost only by default. There is no login yet, and requester names are self-asserted.
|
||
|
||
## Nests
|
||
|
||
A nest is a room where a team of crows works together. You create a nest and pick its core crows; when a job needs more crows than the core has, the Sentry borrows from the rest of the flock. The Sentry and Elder serve every nest and are never core members.
|
||
|
||
- The default nest is `todo`, with an empty core: its crows come from the whole flock, so nothing there is ever "borrowed".
|
||
- Crows are picked core-first (rotating within the core), then borrowed from outside it (rotating), still capped by the flock guardrails.
|
||
- Nests persist on the server (`.crows/nests.json`).
|
||
|
||
| Method | Path | Body | Returns |
|
||
|---|---|---|---|
|
||
| GET | `/api/nests` | | `{"roster": ["Hugin", ...], "nests": [{"nest_id", "name", "crows": [...]}]}`; roster is the 8 crows (not Sentry or Elder) |
|
||
| POST | `/api/nests` | `{"name": "Launch copy", "crows": ["Pepper", "Inky"]}` | 201 with the nest; 400 with `{"message"}` if invalid |
|
||
| POST | `/api/lists` | adds optional `"nest_id"` (default `todo`) | 404 with `{"message"}` for an unknown nest |
|
||
|
||
Guardrails: name 1–40 characters after trimming; `nest_id` is a slug of the name (lowercase, `a-z0-9-`), unique (409 with `{"message"}` on a clash); `crows` must be roster names, no duplicates, 1–8 of them; at most 20 nests.
|
||
|
||
Event changes:
|
||
- `list.started` adds `nest_id`.
|
||
- `crow.flying` adds `borrowed` (bool): true when the crow is not in the nest's core. Always false for Sentry, Elder, and the `todo` nest.
|
||
|
||
## Personas and banter
|
||
|
||
| Method | Path | Returns |
|
||
|---|---|---|
|
||
| GET | `/api/crows` | `[{"name", "epithet", "strength", "backstory"}]` for all ten crows |
|
||
|
||
| Event | Payload (besides list_id, ts) | Meaning |
|
||
|---|---|---|
|
||
| `crow.banter` | `item_id`, `crow` (speaker), `to` (the crow it's addressed to), `text` (one line) | A generated aside between crows. Not part of any answer. |
|
||
|
||
Banter is generated: the server retrieves the involved crows' backstories, their relationship and recent flock events, then the speaking crow writes one line in character. At most 2 per list, counted against the list budget; banter never blocks work, and a failed or unusable line is dropped. `list.finished` waits up to 20 seconds for in-flight banter. See `PERSONAS.md`.
|