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:
@@ -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-<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.
|
||||
`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-<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
|
||||
|
||||
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-<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
|
||||
`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.
|
||||
|
||||
Reference in New Issue
Block a user