From b6022378de27ccd4de9968ba793d8ea539b1e62c Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sat, 3 Oct 2026 18:58:51 -0700 Subject: [PATCH] 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. --- docs/specs/2026-10-04-m6-subagents-consult.md | 127 +++++++++++++----- 1 file changed, 90 insertions(+), 37 deletions(-) diff --git a/docs/specs/2026-10-04-m6-subagents-consult.md b/docs/specs/2026-10-04-m6-subagents-consult.md index 0ba3e9a..c6c81b4 100644 --- a/docs/specs/2026-10-04-m6-subagents-consult.md +++ b/docs/specs/2026-10-04-m6-subagents-consult.md @@ -1,7 +1,7 @@ # M6 design: subagents and cloud consult -Status: proposed. Awaits the owner's answers in section 11. The implementer is Ornith (through -`tools/run-plan.sh`), as for M5d. +Status: proposed. Section 11 records the owner's 2026-10-04 answers; questions 4 and 6 are +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), 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): 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 - the scheduler's background queue. Nested children are out of scope. + one turn (tools allowed), and returns a `ToolResult` that the child completed (id, status, + 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 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. @@ -43,7 +44,7 @@ from a live session instead of by the clock. | Kind | Id | Lives | |---|---|---| | Subagent run | `sub--` | 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. `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) ``` -`task` is capped to the tool-result cap. Unknown keys are errors. The result is the child's -final assistant `content`, tag-neutralised, capped to `[compact] summary_chars`. Thinking is -not returned. If the child ends on a tool call, that is a failed run: the parent sees a -`Failed` result, not a hang. +`task` is capped to the tool-result cap. Unknown keys are errors. The parent turn stays in +this tool call until the child finishes. The `ToolResult` is a completion message: the child +session id, `completed` or `failed`, and on success the child's final assistant `content`, +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` 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 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--` 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 -The summary is the parent's tool result. Nothing is posted to Mattermost. The owner who wants -the child's log opens `sessions/sub-…/`. +The parent learns the child finished the same way it learns any other tool finished: a +`ToolResult` in this turn. Nothing is posted to Mattermost. The owner who wants the child's +full log opens `sessions/sub-…/`. ## 4. Consult (M6b) @@ -138,9 +145,8 @@ session unless the owner writes otherwise. 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 -brief's non-goal is cloud-led orchestration as the default; section 11 asks whether they are -ever allowed later. +Cloud-led sessions (the remote model calling Boxmaker's tools) are **not** in M6. They are +allowed later; the leaning is only at `public` taint. M6's `consult` still has no `tools`. ### 4.2 Audit @@ -204,7 +210,9 @@ Given tests, first, against the fake llama server and a fake `brokerd`: as the newest message. - **Spawn**: `sub-mm-…-3` is created; a second spawn in the same parent is `…-4`. - **Overflow**: a parent id that cannot fit `sub--` 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 `notes.md`. - **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 -- Nested subagents; a pool of children in parallel (one background slot). -- Cloud-led sessions (the remote model calling Boxmaker's tools). Section 11. +- A pool of children in parallel (one background slot). +- Cloud-led sessions in M6 (the remote model calling Boxmaker's tools). Later, leaning `public`. - Optional delivery of a child answer to Mattermost. - Session-scoped approvals (open row in `docs/decisions.md`). - `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 -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: - they are not allowed. Revisit after v0. -- `docs/egress.md`: the consult row in section 4.4. +- `docs/decisions.md` open row "cloud-led sessions": not in M6; later, leaning only at + `public` taint. Leave the row open until that later design. +- `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 - 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 - as written never lets the remote model call tools. Allowed later, and at which taint, or - never in v0? -2. **Child depth.** One child per call, no nested `subagent`. A parent may call `subagent` - several times in one turn, serialised on the background slot. Enough, or a depth-2 tree? -3. **Who waits.** The parent turn waits for the child (and for `consult`). The alternative is - a later `ToolResult` after the parent has already answered, which is a new turn protocol. -4. **Consult URL.** Stub only for the device check, or a real endpoint and secret on - straylight for M6b? -5. **`subagent` vs a different name.** `subagent` matches the milestone table. `delegate` is - the other candidate. -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. +1. **Cloud-led sessions.** Later, not in M6. Leaning `public` taint only. M6 `consult` stays + text-in, text-out, no tools. +2. **Child depth.** Nested `subagent` is allowed. It is the same wait-and-`ToolResult` path: + the waiting session is not using the background slot. Bounded by `tool_iterations` and by + `SessionId` length. A parallel pool is still out of scope. +3. **Who waits.** The parent (or middle child) stays in the tool call. Completion arrives as a + `ToolResult`: session id, completed/failed, and on success the capped summary. Same for + `consult`. No new turn protocol. +4. **Consult URL.** Open; discussion below. +5. **Name.** `subagent`. No reason to pick `delegate`. +6. **Queue.** Open; discussion below. + +### 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.