docs: M6 spec, owner's first answers

Nested subagent allowed; completion is a ToolResult; name stays subagent.
Cloud-led sessions later at public taint. Consult URL and heartbeat
priority still open.
This commit is contained in:
2026-10-03 18:58:51 -07:00
parent 4cb8c8c3c9
commit b6022378de
+90 -37
View File
@@ -1,7 +1,7 @@
# M6 design: subagents and cloud consult # M6 design: subagents and cloud consult
Status: proposed. Awaits the owner's answers in section 11. The implementer is Ornith (through Status: proposed. Section 11 records the owner's 2026-10-04 answers; questions 4 and 6 are
`tools/run-plan.sh`), as for M5d. still open. The implementer is Ornith (through `tools/run-plan.sh`), as for M5d.
It rests on the brief (`docs/design.md`, Inference contract 5 and 6, Authority 6, Cloud consult), It rests on the brief (`docs/design.md`, Inference contract 5 and 6, Authority 6, Cloud consult),
on M5d's scheduler (`docs/specs/2026-10-03-m5d-scheduler.md`, out of scope: subagents join that on M5d's scheduler (`docs/specs/2026-10-03-m5d-scheduler.md`, out of scope: subagents join that
@@ -26,8 +26,9 @@ The milestone is checked by:
One spec, two plans, each run and device-checked on its own (owner): One spec, two plans, each run and device-checked on its own (owner):
1. **Subagents (M6a)**: a `subagent` tool starts an isolated session on `slots.background`, runs 1. **Subagents (M6a)**: a `subagent` tool starts an isolated session on `slots.background`, runs
one turn (tools allowed), and returns a capped summary as the parent's `ToolResult`. It joins one turn (tools allowed), and returns a `ToolResult` that the child completed (id, status,
the scheduler's background queue. Nested children are out of scope. capped summary). It joins the scheduler's background queue. A child may call `subagent`
(nested); the background slot is free while a parent waits, so this is the same code path.
2. **Consult (M6b)**: a `consult` tool, decided by `brokerd`, POSTs an OpenAI-compatible 2. **Consult (M6b)**: a `consult` tool, decided by `brokerd`, POSTs an OpenAI-compatible
chat-completions body built from an allowlist of fields. The grant's `max_taint` is the class chat-completions body built from an allowlist of fields. The grant's `max_taint` is the class
gate. The audit log holds the request body and the answer. gate. The audit log holds the request body and the answer.
@@ -43,7 +44,7 @@ from a live session instead of by the clock.
| Kind | Id | Lives | | Kind | Id | Lives |
|---|---|---| |---|---|---|
| Subagent run | `sub-<parent>-<n>` | one session per call; `n` is the parent's next `CallId` | | Subagent run | `sub-<parent>-<n>` | one session per call; `n` is the parent's next `CallId` |
| Parent | unchanged (`mm-*`, `chat-*`, `job-*`, `heartbeat`) | holds until the child `TurnDone` | | Parent | unchanged (`mm-*`, `chat-*`, `job-*`, `heartbeat`, or another `sub-*`) | holds until the child `TurnDone` |
`SessionId` is `[a-z0-9-]`, max 64. A Mattermost parent id is `mm-` plus 26 characters. `SessionId` is `[a-z0-9-]`, max 64. A Mattermost parent id is `mm-` plus 26 characters.
`sub-` + parent + `-` + a decimal call id stays under 64. A parent that would overflow is `sub-` + parent + `-` + a decimal call id stays under 64. A parent that would overflow is
@@ -62,10 +63,11 @@ user text, with the same time line M5d prepends so the clock stays out of the ch
subagent(task: string) subagent(task: string)
``` ```
`task` is capped to the tool-result cap. Unknown keys are errors. The result is the child's `task` is capped to the tool-result cap. Unknown keys are errors. The parent turn stays in
final assistant `content`, tag-neutralised, capped to `[compact] summary_chars`. Thinking is this tool call until the child finishes. The `ToolResult` is a completion message: the child
not returned. If the child ends on a tool call, that is a failed run: the parent sees a session id, `completed` or `failed`, and on success the child's final assistant `content`,
`Failed` result, not a hang. tag-neutralised, capped to `[compact] summary_chars`. Thinking is not returned. If the child
ends on a tool call, that is `failed`, not a hang.
`loopd` answers `subagent` itself (it is the only role that can start inference). `brokerd` `loopd` answers `subagent` itself (it is the only role that can start inference). `brokerd`
does not see the spawn; every tool the child then calls still goes to `brokerd` as today. does not see the spawn; every tool the child then calls still goes to `brokerd` as today.
@@ -96,12 +98,17 @@ an identity write from a tainted parent is still a note (existing M5a rule).
No speaker on the child, so Honcho skips it (same skip as heartbeat). The local index still No speaker on the child, so Honcho skips it (same skip as heartbeat). The local index still
holds the log. `recall` in the child is ordinary. holds the log. `recall` in the child is ordinary.
A child does not spawn a child: `subagent` in a `sub-*` session is denied as invalid-arguments. A child may call `subagent`. The child's inference request has already finished when it waits
on that tool, so the background slot is free for the grandchild; there is no deadlock with one
slot. Depth is bounded by the parent's `tool_iterations` along the call stack and by
`SessionId` (64 bytes): `sub-<parent>-<n>` nested a few times still fits a `mm-*` parent, then
invalid-arguments. A parallel pool of children is still out of scope.
### 3.5 Delivery ### 3.5 Delivery
The summary is the parent's tool result. Nothing is posted to Mattermost. The owner who wants The parent learns the child finished the same way it learns any other tool finished: a
the child's log opens `sessions/sub-…/`. `ToolResult` in this turn. Nothing is posted to Mattermost. The owner who wants the child's
full log opens `sessions/sub-…/`.
## 4. Consult (M6b) ## 4. Consult (M6b)
@@ -138,9 +145,8 @@ session unless the owner writes otherwise.
The result class is the grant's `result_class` (default `private`). The result class is the grant's `result_class` (default `private`).
Cloud-led sessions (the model at the far end driving Boxmaker's tools) are **not** in M6. The Cloud-led sessions (the remote model calling Boxmaker's tools) are **not** in M6. They are
brief's non-goal is cloud-led orchestration as the default; section 11 asks whether they are allowed later; the leaning is only at `public` taint. M6's `consult` still has no `tools`.
ever allowed later.
### 4.2 Audit ### 4.2 Audit
@@ -204,7 +210,9 @@ Given tests, first, against the fake llama server and a fake `brokerd`:
as the newest message. as the newest message.
- **Spawn**: `sub-mm-…-3` is created; a second spawn in the same parent is `…-4`. - **Spawn**: `sub-mm-…-3` is created; a second spawn in the same parent is `…-4`.
- **Overflow**: a parent id that cannot fit `sub-<parent>-<n>` is invalid-arguments. - **Overflow**: a parent id that cannot fit `sub-<parent>-<n>` is invalid-arguments.
- **Nested**: `subagent` from a `sub-*` session is denied. - **Nested**: `subagent` from a `sub-*` session creates a grandchild; the middle child's
`ToolResult` contains the grandchild's completion message; the live parent's prefix still
extends. An id that would exceed 64 bytes is invalid-arguments.
- **Taint copy**: a parent with `untrusted` true has a child whose identity `add` lands in - **Taint copy**: a parent with `untrusted` true has a child whose identity `add` lands in
`notes.md`. `notes.md`.
- **Consult deny**: a session at `secret` with a `consult` grant `max_taint = "private"` is - **Consult deny**: a session at `secret` with a `consult` grant `max_taint = "private"` is
@@ -236,8 +244,8 @@ New entries, each with a pointer from the message:
## 9. Out of scope ## 9. Out of scope
- Nested subagents; a pool of children in parallel (one background slot). - A pool of children in parallel (one background slot).
- Cloud-led sessions (the remote model calling Boxmaker's tools). Section 11. - Cloud-led sessions in M6 (the remote model calling Boxmaker's tools). Later, leaning `public`.
- Optional delivery of a child answer to Mattermost. - Optional delivery of a child answer to Mattermost.
- Session-scoped approvals (open row in `docs/decisions.md`). - Session-scoped approvals (open row in `docs/decisions.md`).
- `consult` with tools, streaming to Mattermost, or a vendor SDK. - `consult` with tools, streaming to Mattermost, or a vendor SDK.
@@ -245,26 +253,71 @@ New entries, each with a pointer from the message:
## 10. Changes to the brief and decisions ## 10. Changes to the brief and decisions
With this spec, as their own commits, if section 11 agrees: With this spec, as their own commits, once 4 and 6 are settled:
- Close "Whether cloud-led sessions are ever allowed, and for which data classes" for v0: - `docs/decisions.md` open row "cloud-led sessions": not in M6; later, leaning only at
they are not allowed. Revisit after v0. `public` taint. Leave the row open until that later design.
- `docs/egress.md`: the consult row in section 4.4. - `docs/egress.md`: the consult row in section 4.4, once the URL is chosen.
- Inference contract 5 already says subagents share the background slot; no wording change - Inference contract 5 already says subagents share the background slot; no wording change
unless section 11 puts children on a third slot. unless question 6 puts heartbeat on that slot ahead of a waiting live child.
## 11. The owner's questions (2026-10-04) ## 11. The owner's answers (2026-10-04)
1. **Cloud-led sessions.** The brief's non-goal is cloud-led orchestration as the default. M6 1. **Cloud-led sessions.** Later, not in M6. Leaning `public` taint only. M6 `consult` stays
as written never lets the remote model call tools. Allowed later, and at which taint, or text-in, text-out, no tools.
never in v0? 2. **Child depth.** Nested `subagent` is allowed. It is the same wait-and-`ToolResult` path:
2. **Child depth.** One child per call, no nested `subagent`. A parent may call `subagent` the waiting session is not using the background slot. Bounded by `tool_iterations` and by
several times in one turn, serialised on the background slot. Enough, or a depth-2 tree? `SessionId` length. A parallel pool is still out of scope.
3. **Who waits.** The parent turn waits for the child (and for `consult`). The alternative is 3. **Who waits.** The parent (or middle child) stays in the tool call. Completion arrives as a
a later `ToolResult` after the parent has already answered, which is a new turn protocol. `ToolResult`: session id, completed/failed, and on success the capped summary. Same for
4. **Consult URL.** Stub only for the device check, or a real endpoint and secret on `consult`. No new turn protocol.
straylight for M6b? 4. **Consult URL.** Open; discussion below.
5. **`subagent` vs a different name.** `subagent` matches the milestone table. `delegate` is 5. **Name.** `subagent`. No reason to pick `delegate`.
the other candidate. 6. **Queue.** Open; discussion below.
6. **Queue.** Child-of-a-held-parent beats cron and heartbeat. Cron-before-child would stall a
live Mattermost turn behind a weekly tiering job. ### Consult URL (question 4)
Three shapes, and they can combine.
- **Given tests** always hit a fake HTTP server in-process. That does not need a network or a
secret.
- **Device check, stub.** A private OpenAI-compatible listener on straylight (loopback, one
shot) so M6b proves audit-body == wire bytes without sending owner text off the host. Enough
to close the milestone's "payload log matches what was sent".
- **Device check, live.** A real `[consult] url` and secret on straylight. The brief's
candidate is GLM-5.3-Flash over Z.ai pay-as-you-go (Singapore entity; they state they do not
store API content). That is the first new internet egress since `http_fetch`: it needs a
grant, an `egress.md` row, and a secret in the store. It also means the device check sends a
planted `question` to that host.
Recommendation: stub for the M6b device check (the audit match is a property of `brokerd`, not
of GLM). Add a live URL later, when you want Ornith to actually consult, as config and a grant,
without another milestone. A live check can still be a follow-up task if you want the first
real call witnessed.
If we do live in M6b, the planted question should be boring and public (`What is 2+2, one
word.`), and the grant `max_taint = "public"` so a private session cannot use it until you
raise that on purpose.
### Heartbeat vs subagent (question 6)
M5d's order on a contended background slot is compaction, then cron, then heartbeat, then a
tool-output summary. A subagent serving a **held live parent** is the owner's current turn
waiting on the slot. Heartbeat is the pulse for when the owner is not talking.
Heartbeat-first would mean a Mattermost turn that called `subagent` waits out a heartbeat
(often several tool calls; the M5d-2 check saw ~20 s of `recall`s) and, with `last_heartbeat`
stamped at tick start, a long pulse can come due again immediately. The live user pays that.
Subagent-first (the spec's current list) delays heartbeat until the child finishes. That is
the same as today when a compaction summary or a cron run holds the slot: heartbeat skips or
waits. Heartbeat is already written to skip when its own session is busy.
Cron is owner-scheduled and can be a long tiering turn. Putting cron ahead of a held live
child has the same stall. Compaction in flight should stay first: it is already holding a
session the way a child is, and it unblocks that session's next live turn.
Recommendation: keep compaction first, then a subagent whose parent is held, then cron, then
heartbeat, then tool-output summaries. Heartbeat should not take precedence over a child that
a live turn is waiting on. It should still beat a child whose parent is **not** held, if we
ever queue one; under answer 3 we do not, because the parent stays in the tool call.