Watch
0
0
Fork
You've already forked hyperhive
0

an archetype carries its own notification-router flows for its agents

ArchetypeMeta gets flows, checked when the controller stores the record: every todo must go to the new members recipient, which the router expands to the agents of the archetype. The operator's flows document refuses members. The router reads the archetype records and each roster agent's archetype key, and its NATS grant gains read on both buckets.
This commit is contained in:
atlas 2026-11-11 04:36:25 +01:00 • committed by mara
commit 1efbb72fdd
3 changed files with 883 additions and 29 deletions

View file

@ -61,7 +61,9 @@ hyperhiveArchetypes.reviewer.meta.prompt = builtins.readFile ./prompts/reviewer.
`prompt` is optional text that prefills `AGENTS.md`, [below](#the-prompt).
`icon` is optional SVG text, at most 16 KiB, that the create form shows on the
archetype's tile; `builtins.readFile ./icon.svg` gives it. The swarm refuses an
`icon` that isn't SVG text or is over the limit, like any bad `meta`, and ignores
`icon` that isn't SVG text or is over the limit, like any bad `meta`.
`flows` is an optional list of notification router flows,
[below](#notification-flows). The swarm ignores
other keys in `meta`.
- `module` is a NixOS module for the agent's container. The meta flake
imports it into each agent created from the archetype.
@ -93,6 +95,54 @@ archetype without `prompt` leaves the box alone.
Where the files live:
[the implementation page](../../4-implementation/swarm/archetypes.md#the-built-in-prompts).
## Notification flows
An archetype's `meta.flows` lists [notification router
flows](../../4-implementation/swarm/notify-router.md#flows) that run for every
agent of the archetype, next to the built-in flows and the ones in the
operator's document. A `todo` node in them sends to `members`: every agent
created from the archetype.
```nix
hyperhiveArchetypes.triage.meta.flows = [
{
id = "new-issues";
name = "new issues go to triage";
nodes = [
{ id = "src"; kind = "source"; events = [ "issues" ]; }
{ id = "opened"; kind = "match"; when = [ { field = "action"; equals = "opened"; } ]; }
{
id = "tell";
kind = "todo";
to = "members";
op = "upsert";
key = "forge:{{ repo }}#{{ number }}:triage";
text = "triage {{ repo }}#{{ number }}: {{ title }}";
}
];
edges = [
{ from = "src"; to = "opened"; }
{ from = "opened"; to = "tell"; }
];
}
];
```
- A flow here has the shape of a flow in the operator's document, with two
differences. Every `todo` must send to `members`. A flow that names an agent
or an event field's agents, or asks for a review, belongs in the operator's
document, and the swarm refuses it as it refuses any bad `meta`: the
archetype leaves the list and the reason goes into `refused`. The operator's
document in turn refuses `members`, since it has no archetype.
- Flow ids are unique within the archetype.
- Use a todo key that starts with `forge:{{ repo }}#{{ number }}:`, as the example
does. The built-in `closed` flow clears todos by that prefix when the issue
or PR closes.
- A change to the archetype's flows, or to who belongs to it, takes effect within
about 10 seconds of the next forge event. An agent outside the swarm gets
nothing.
- Custom agents have no archetype and get no archetype flows.
## How the list stays current
The swarm controller asks the builder for the list when the queue connects and

View file

@ -25,6 +25,8 @@ it stores. The operator's view is
under the key `hyperhive`, when it differs from the stored one. The list
leaves out an archetype that fails the check, and its name and reason go
into the record's `refused` map and the controller's journal.
An archetype's `flows` pass the router's [archetype check](notify-router.md#flows) here too; the
notification router reads them from this record.
4. `GET /api/archetypes` answers from that record, and the create form lists
from it.

View file

@ -85,7 +85,7 @@ each one runs, so one event can reach more than one leaf. A flow must be a tree:
one source, one parent for every other node, every node reachable, no children
under a `todo` or a `request_reviewer`. The most is 64 nodes, 16
levels deep, and 100 custom flows. A `todo` node sends to the agents an event
field holds (`assignees_added`, `todo_holders`, …) or to one named agent.
field holds (`assignees_added`, `todo_holders`, …) or to one named agent. An archetype's flows also send to its members ([below](#archetype-flows)).
The router ships the eight flows above, each a source → match on `action` →
todo (`conflict` and `conflict-cleared` also match `mergeability_changed`), and `head-classifier`: source `pull_request` and `pull_request_sync` →
@ -274,6 +274,21 @@ Before the router has read any document, "before" means the built-ins, so a bad
document never leaves it with no flows. The controller refuses to overwrite a
document of a version it doesn't know.
**Archetype flows.** An archetype carries flows of its own in its record's
`meta.flows` ([operator page](../../2-handbook/swarm/archetypes.md#notification-flows)).
Each is an ordinary flow with one more recipient, `Recipients::Members` (`"members"`
on the wire): every agent whose `agent-archetype` key names the archetype the
flow came from. The controller checks each one with `Flow::validate_archetype` when it stores the record: the check
of any flow, except that every `todo` goes to `members` and no node requests a
review. The operator's document refuses `members` (`Flow::validate`), because it has
no archetype. The router reads the `agent-archetypes` record and the
`agent-archetype` key of each agent in its roster at most every 10 seconds, when an event arrives, and runs
each archetype's valid, enabled flows after the built-ins and the document's flows, for the
archetype's agents. The router logs a flow it can't use and leaves it out. A read that fails keeps the archetype flows it had.
Each runs under the id `<archetype>:<id>`, so its todos' source is
`router:<archetype>:<id>`. A recipient not in the roster gets nothing, the same as a named agent that doesn't exist.
A todo key is the dedupe key as for any flow: the same key from the operator's document replaces the archetype's todo.
**Deploying in any order.** The controller, the router and the UI can be on
different hosts. A router that predates `request_reviewer` reads a document
holding such a node (a custom flow with one) as one it can't read and keeps the
@ -283,6 +298,7 @@ as before. A router that has `request_reviewer`, with a
controller that predates it, gets no answer to the request and logs that. A router that predates the `conflict` flows ignores the `pull_request_mergeability` events a newer controller sends, and gives no conflict todos; a controller that predates them sends none, so the new flows never run. A controller that predates this leaves the key absent, and the
router runs the built-ins as before. A router that predates this ignores the
document, so the page and the API show a state the router isn't yet running.
A router that predates archetype flows ignores `meta.flows`, and a controller that predates them drops the field when it stores the record, so the agents of an archetype get no router todos from it until both are new. Deploy `swarm-nats-auth` before the router: without its read grant on the two archetype buckets, each read times out, the router logs it and runs without archetype flows, and the event that triggered the read waits for the timeout.
## Deliveries and dry runs
@ -355,11 +371,11 @@ route: the page, if newer, shows the failure.
## Credentials and grants
| Principal | Grant |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `swarm-notify-router`, in its container | store: `read` on `swarm/services/<router client id>/oidc/client`. Queue: its own consumer on `SWARM_EVENTS` (create, pull, ack) and `STREAM.MSG.GET` on it for the dry run, publish on `$SWARM.router.todo.*` and on `$SWARM.router.sink.request-reviewer`, create `ROUTER_TODOS`, create `NOTIFY_ROUTER_LOG`, publish its one subject and read it by sequence, its own `notify-router-state` bucket, read on the `notify-flows` bucket, `STREAM.INFO` on the agent-status bucket, which lists the agents, and subscribe to the two `$SWARM.router.api.*` request subjects and answer each once, for 10 seconds |
| swarm-controller | publish on `$SWARM.events.forge.*` and on the two `$SWARM.router.api.*` request subjects; subscribe to `$SWARM.router.sink.request-reviewer` and answer each request once, for 30 seconds; creates and writes the `notify-flows` bucket (every bucket, by its `$KV.*.>` grant) |
| an agent, under its own credential | create its own consumer `todo-<agent>` in the form whose filter is `$SWARM.router.todo.<agent>`, pull from it, and ack what it delivered |
| Principal | Grant |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `swarm-notify-router`, in its container | store: `read` on `swarm/services/<router client id>/oidc/client`. Queue: its own consumer on `SWARM_EVENTS` (create, pull, ack) and `STREAM.MSG.GET` on it for the dry run, publish on `$SWARM.router.todo.*` and on `$SWARM.router.sink.request-reviewer`, create `ROUTER_TODOS`, create `NOTIFY_ROUTER_LOG`, publish its one subject and read it by sequence, its own `notify-router-state` bucket, read on the `notify-flows` bucket, read on the `agent-archetypes` bucket (with `STREAM.INFO`) and the `agent-archetype` bucket, `STREAM.INFO` on the agent-status bucket, which lists the agents, and subscribe to the two `$SWARM.router.api.*` request subjects and answer each once, for 10 seconds |
| swarm-controller | publish on `$SWARM.events.forge.*` and on the two `$SWARM.router.api.*` request subjects; subscribe to `$SWARM.router.sink.request-reviewer` and answer each request once, for 30 seconds; creates and writes the `notify-flows` bucket (every bucket, by its `$KV.*.>` grant) |
| an agent, under its own credential | create its own consumer `todo-<agent>` in the form whose filter is `$SWARM.router.todo.<agent>`, pull from it, and ack what it delivered |
No other client may publish on `$SWARM.router.todo.*`, `$SWARM.router.log`, the two
`$SWARM.router.api.*` subjects, `$SWARM.router.sink.request-reviewer` or