Harness spec 3.4.0: progress phases, and subagents v2 — the main agent and its tree
ci / check (push) Waiting to run
ci / check (push) Waiting to run
progress.md: thirteen fixed phases (queued … failed), each { phase, since, data },
their texts as shared i18n keys (loop.json progress), one line every client draws,
sent beside the old status text in /api/v2 and over the link (capability progress).
subagents.md §5–§13 (supersedes team.md where they differ): only the main agent
adds, assigns, replaces, stops and decides; sub-managers direct those placed under
them; ids, positions, parents; Min above 1 opens slots; permissions inherited and
never widened, request options decided by the main agent, what nobody in the tree
can allow goes to the person; team_finish, findings, running out of context;
transcripts read-only; the tree over the link (capability tree). New tools
team_whoami, team_finish, team_findings, team_request, team_decide, team_assign,
team_replace. Conformance: progress, delegation; team updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
26 files changed
+1844
-97
No files matched your search
+261
-1
@@ -1,6 +1,6 @@
|
||||
# Subagents
|
||||
|
||||
Spec 3.3.0. **Which model a subagent runs on, and when subagents are offered at all.** A
|
||||
Spec 3.3.0; §5–§13 spec 3.4.0. **Which model a subagent runs on, and when subagents are offered at all.** A
|
||||
**subagent** is any agent a session's agent starts to work for it: a `task` subagent (in the
|
||||
foreground or as a background job) and a team's member (`team.md`) alike. The same rules in a chat
|
||||
on the server and in an agent session on a device, in both projects. The texts are `loop.json`
|
||||
@@ -88,3 +88,263 @@ before 3.3.0 for a single model.
|
||||
|
||||
What counts as **usable** is each project's: LLeMbas — the models the person may use in the chat's
|
||||
picker; the CLI — the models of the server it is logged in to.
|
||||
|
||||
---
|
||||
|
||||
# Subagents v2 — the main agent and its tree
|
||||
|
||||
Spec 3.4.0. **How subagents are organised, what each may do, and how each ends.** The owner's model:
|
||||
one permanent **main agent** per session, which alone makes subagents and places them in a tree;
|
||||
permissions are inherited and never widened; a subagent is temporary and ends with its task or its
|
||||
context. Sections 5–13 **supersede `team.md` where they say something different** — who adds a
|
||||
subagent (§6), who stops one (§10), the states (§9), what a member is told (§13) — and keep the
|
||||
rest of it: the size setting and its counting (§2–§3), the room and who receives what (§5),
|
||||
delivery and waking (§7), a manager's refusals (§6 there), capacity and waiting for a slot (§8).
|
||||
The tool names `team_*` stay (nothing stored or trained breaks); `task` and `report` are unchanged
|
||||
for a one-off subagent. The texts are `loop.json` `team` (3.4.0 additions marked there); the cases
|
||||
`conformance/team.json` (the tree, the tools, the lifecycle) and `conformance/delegation.json`
|
||||
(permissions).
|
||||
|
||||
## 5. Words
|
||||
|
||||
- **The main agent** is the session's own agent: the chat's agent on LLeMbas, the session's in the
|
||||
CLI. It is **permanent**: it compacts and never ends. In the room and in every tool it is called
|
||||
`lead` (the name is kept from 3.2.0); a person reads it as *the main agent*.
|
||||
- **A subagent** is an agent the main agent added (`team_add`). It has:
|
||||
- **`id`** — `s1`, `s2`, … in the order the session's subagents were added, never reused in the
|
||||
session (a replacement gets a new one);
|
||||
- **`name`** — the main agent's choice (`team.names.pattern`), unique among the subagents that
|
||||
have not ended, `lead`, `all` and `parent` reserved;
|
||||
- **`position`** — its designation, one line, given by the main agent (`team_add`'s `role`:
|
||||
*the API's tests*, *reviews the parser*);
|
||||
- **`parent`** — the main agent, or a **sub-manager** the main agent placed it under;
|
||||
- **`task`** — its brief, as given (and as replaced by `team_assign`);
|
||||
- **`mode`** and **`request`** — its permission mode and its request options (§7);
|
||||
- **`model`** — `subagents.md` §2–§3, unchanged.
|
||||
- **A sub-manager** is a subagent added with `lead: true`. It **directs** the subagents the main
|
||||
agent places under it — they report to it, it messages them, it reviews their work — and it does
|
||||
**not** add, assign, replace or stop subagents: only the main agent does (§6). Like every manager
|
||||
it does not do the work itself (`team.md` §6, unchanged).
|
||||
- **The tree**: the main agent at the root, every subagent under its parent, in the order added.
|
||||
**Under** an agent and **its subtree** keep their `team.md` meaning.
|
||||
|
||||
## 6. Only the main agent makes subagents
|
||||
|
||||
- **`team_add`, `team_assign`, `team_replace`, `team_stop` and `team_decide`** are the main agent's.
|
||||
A subagent's call to any of them is refused, word for word, with `team.texts.only_main`:
|
||||
|
||||
> Only the main agent adds, assigns, replaces and stops subagents: ask it (team_send to lead).
|
||||
|
||||
A subagent's tool list never has `task` (a one-off subagent is the main agent's too), and a call
|
||||
to it by a former name is refused with the same text.
|
||||
- **`team_add`** takes, beside 3.2.0's `name`, `role` (the position), `task`, `goal`, `lead`,
|
||||
`agent`, `model`: **`parent`** — the name of a sub-manager to place it under (default: the main
|
||||
agent; refused with `team.texts.not_manager` for a subagent that is not a sub-manager, and
|
||||
`unknown` for no such name), **`mode`** — its mode (default: its parent's; §7), **`request`** —
|
||||
its request options (§7). Its refusals, in order: `unavailable`, `only_main`, then 3.2.0's (`all_name`,
|
||||
`bad_name`, `taken`, `no_goal`, `full`), then `unknown` and `not_manager` for `parent`, then the mode's (`wider`, `request_not_narrower`,
|
||||
`request_kind`), then the model's (§2–§3). `parent` and the reserved names are refused as
|
||||
`taken`.
|
||||
- **The person** can tell the main agent to spawn — it is the main agent's call, as any — or set
|
||||
**Min above 1**: then **`min − 1` slots** exist from the start of the main agent's reply (the
|
||||
first one after the setting was told, `team.md` §10). A **slot** is a place in the tree for a
|
||||
subagent not added yet: it is shown (§12) and `team_read` lists it, it is **not** an agent and
|
||||
does not count toward the size, and each subagent added fills one. Open slots are
|
||||
`max(0, min − 1 − subagents that have not ended)`. The main agent is told to use them in its
|
||||
note (`context.note.team_lead`, which 3.4.0 words as slots), and is a manager from the start
|
||||
(`team.md` §6).
|
||||
- **Min/Max** (3.3.0) are unchanged: they count the main agent; 1 is the main agent alone. The
|
||||
model rules (§2–§3) are unchanged.
|
||||
|
||||
## 7. Permissions: inherited, never widened
|
||||
|
||||
**A subagent's mode is its parent's or stricter** — `permission/modes.json` `order`, loosest first:
|
||||
`auto`, `edit`, `manual`, `plan`. The main agent's mode is the session's.
|
||||
|
||||
- `team_add`'s and `team_assign`'s `mode`, looser than the parent's, is refused, word for word,
|
||||
with `team.texts.wider`:
|
||||
|
||||
> A subagent's mode cannot be looser than its parent's ({{parent_mode}}).
|
||||
|
||||
When the parent's own mode becomes stricter during the session (the person switches the main
|
||||
agent to manual), every subagent looser than its parent is made as strict as it, down the tree,
|
||||
at once; a looser switch changes no subagent.
|
||||
- **Request options** (`request`: a list of the risk classes `read`, `write`, `execute`) may be
|
||||
given **only with a mode stricter than the parent's** — otherwise refused with
|
||||
`team.texts.request_not_narrower` ("Request options go with a mode stricter than the parent's:
|
||||
without one there is nothing to ask for."); an unknown kind with `team.texts.request_kind`. They
|
||||
let the subagent **ask the main agent** for a call of that class that its own mode does not
|
||||
allow; **the main agent is the check**.
|
||||
|
||||
**Who decides a subagent's call.** The hardline and the deny rules first, as for any call. Then:
|
||||
|
||||
1. The call is evaluated in the **subagent's mode**, with the session's rules and approvals and
|
||||
the subagent's own grants (below). **Allow** — it runs. **Deny** (a rule, the hardline, plan
|
||||
mode's refusal) — refused, as always.
|
||||
2. **Ask** — the call is evaluated in the **main agent's mode** (the session's rules and
|
||||
approvals, no grants):
|
||||
- **Allow** — the main agent could allow it: the subagent was narrowed. If the call's risk class
|
||||
is in its `request`, refused with `team.texts.requestable` — it asks with `team_request`, the
|
||||
main agent decides with `team_decide`, and once allowed the same call runs. Otherwise refused
|
||||
with `team.texts.not_delegated`:
|
||||
|
||||
> Your mode does not allow this, and you may not ask for it: tell your parent what you need.
|
||||
|
||||
- **Ask** — **nobody in the tree can allow it: it goes to the person**, as an approval card that
|
||||
names the subagent (its name and position), the same card and the same answers as the main
|
||||
agent's own; unattended, refused as `modes.json` `unattended` says. So in an all-Edit tree a
|
||||
subagent's `bash` reaches the person — not the main agent, which could not allow it either.
|
||||
- **Deny** — refused with that reason.
|
||||
|
||||
A sub-manager is never asked: it may not allow more than the main agent, and the main agent is
|
||||
the check.
|
||||
|
||||
**`team_request`** (a subagent with request options; its tool list has it only then): `tool`, the
|
||||
permission `pattern` it needs (as a rule writes it: `git push *`, a path) and `why`. Refused with
|
||||
`team.texts.not_requestable` when the tool's risk class is not in its options. Posted to the main
|
||||
agent as a request, under its own mark (`team.marks.request`), the body `team.texts.request_body`,
|
||||
with an id (`r1`, `r2`, … per session); the answer `team.texts.requested` says it waits for the main agent's decision.
|
||||
|
||||
**`team_decide`** (the main agent): `id`, `allow` (a boolean), `note`. Allowed — the subagent gets
|
||||
a **grant**: the rule `{ permission: <tool's key>, pattern, action: allow }` for itself alone, for
|
||||
the rest of its life; **a grant only ever applies where the main agent's own mode would allow the
|
||||
call** (step 2's evaluation, done again at the call), so it can never widen past the main agent.
|
||||
Either way the subagent is told, under `team.marks.decision`: `team.texts.decision_allowed` or
|
||||
`decision_refused`, then the note after a blank line. The answer to the main agent is
|
||||
`team.texts.decided` (`decision` being `allowed` or `refused`). Refused:
|
||||
`team.texts.no_request` for an id that is not an open request.
|
||||
|
||||
## 8. What each agent has
|
||||
|
||||
| Tools | Main agent | Sub-manager | Subagent |
|
||||
|---|---|---|---|
|
||||
| `team_whoami`, `team_read`, `team_send`, `team_findings` | ✓ | ✓ | ✓ |
|
||||
| `team_finish` | — | ✓ | ✓ |
|
||||
| `team_request` | — | with request options | with request options |
|
||||
| `team_add`, `team_assign`, `team_replace`, `team_stop`, `team_decide` | ✓ | — | — |
|
||||
|
||||
Fixed when the prompt is made (the main agent's) or when the subagent is added (`context.md` §10,
|
||||
rule 4). What each does:
|
||||
|
||||
- **`team_whoami`** — who the caller is: `team.whoami`, one line each, in its order — id, name, position, parent, task, mode and
|
||||
request options, model, the limits (the size and Max, the steps a turn may take, whether it may
|
||||
add subagents) and its **context use** (tokens in its context now, of its model's window, and the
|
||||
percentage). For the main agent: the same, its position *the main agent*, without the parent and task lines.
|
||||
- **`team_read`** — the roster: the tree (every agent and open slot, §12's fields) and the room,
|
||||
as in 3.2.0 (`team.read`; 3.4.0's row adds the id, the mode and the context use).
|
||||
- **`team_send`** — a message `to` names, `all`, or **`parent`** (3.4.0): the caller's parent
|
||||
(refused for the main agent with `team.texts.no_parent`). Otherwise 3.2.0.
|
||||
- **`team_finish`** — `report`: the subagent's task is done. The report goes to its parent (the
|
||||
report mark, as a final reply's), and the subagent **ends** (§9): its turn stops after this call,
|
||||
it leaves the size, and its findings and transcript stay.
|
||||
- **`team_findings`** — `write` (`text`): a finding kept for the caller, numbered from 1; `read`
|
||||
(`of`: a name or an id, default the caller): the findings of the caller, or of an agent in its
|
||||
subtree (the main agent: anybody's), ended ones included — as `team.findings` writes them. A
|
||||
name that has been used more than once means its latest holder; an id is exact. Otherwise refused with
|
||||
`team.texts.not_yours_findings`. The person reads every agent's (§12).
|
||||
- **`team_assign`** — `name`, `task`, and optionally `role` (a new position), `parent` (another
|
||||
sub-manager, or `lead`), `mode` (§7): a new task for a subagent that has not ended. Its task
|
||||
becomes this one; it is told under `team.marks.assignment`, and a finished one is woken. Refused:
|
||||
`unknown`, `team.texts.ended` for one that has ended, `not_manager`, `wider`; a move under its
|
||||
own subtree with `team.texts.cycle`.
|
||||
- **`team_replace`** — `name`, and optionally `task`: the subagent ends (stopped, if it has not
|
||||
already), and a new one takes its place — the same name, position, parent, mode, request options
|
||||
and model, a new id — briefed with `task` (default: the old task) followed by the old one's
|
||||
findings (`team.texts.replace_brief`). Its subagents move under the new one. Refused: `unknown`.
|
||||
- **`team_stop`** — the main agent's only (3.2.0 let a manager stop its subtree); otherwise 3.2.0.
|
||||
- **`team_decide`** — §7.
|
||||
|
||||
## 9. A subagent's life
|
||||
|
||||
States (`team.read.states`): **`working`** (a turn runs), **`waiting`** (it has work and waits for
|
||||
a slot, `team.md` §8), **`finished`** (its last turn ended with a final reply, which was its report;
|
||||
a message wakes it — 3.2.0), **`paused`** (its context is full; its last turn runs), and the ends:
|
||||
**`done`** (`team_finish`), **`out_of_context`** (its last turn after `paused` ended), **`stopped`**
|
||||
(the main agent's `team_stop`, a replacement, or the person). The main agent is `working`,
|
||||
`waiting` or `idle`.
|
||||
|
||||
**An ended subagent** — `done`, `out_of_context`, `stopped` — leaves the size and its slot frees;
|
||||
its name may be used again; it keeps its id, and the tree keeps it (§12) with its findings and
|
||||
transcript for the rest of the session. Messages to it by name are refused as `unknown`.
|
||||
|
||||
**When its context fills.** A subagent does not compact. When its context reaches
|
||||
`team.context.pause_at` of its model's window (default 0.85, the fraction compaction would start
|
||||
at), after the step that took it there:
|
||||
|
||||
1. it is **`paused`**: nothing new reaches it — a message to it is posted in the room and not
|
||||
delivered; whoever replaces it reads it with `team_read`;
|
||||
2. it is given **one final turn**, at most `team.context.final_steps` steps (8), whose user message
|
||||
is `team.texts.context_full`, under its own mark: save what it found with `team_findings`, and
|
||||
tell its parent with `team_send` that it is gone and what is left;
|
||||
3. when that turn ends it is **`out_of_context`**, and its parent is sent `team.texts.gone` — then,
|
||||
after a blank line, that turn's final reply if it gave one — as its report (the report mark): the name, that it ran out of context, and how to read its
|
||||
findings. **The parent decides** — the main agent replaces it (`team_replace`) or uses the
|
||||
findings; a sub-manager tells the main agent.
|
||||
|
||||
**The main agent is permanent**: it compacts as every session does (`context.md` §7) and never
|
||||
ends; deleting the session ends every subagent.
|
||||
|
||||
**Waiting for subagents.** The main agent's turn may end while subagents work; their reports wake
|
||||
it (3.2.0 §7). Its progress is `waiting_for_subagents` (`progress.md`) while its turn waits on
|
||||
them — a foreground `task` — and the session shows the subagents' own phases in the tree.
|
||||
|
||||
## 10. Stopping
|
||||
|
||||
Only the main agent stops (`team_stop`, 3.2.0's refusals but `not_yours`, which no longer arises),
|
||||
and **the person** (§12) — said to the main agent as a note (`context.note.team_stopped`). A
|
||||
stopped subagent's subtree is stopped with it, unless the main agent moved it first. A sub-manager
|
||||
that wants a subagent of its own stopped asks the main agent.
|
||||
|
||||
## 11. Transcripts and findings
|
||||
|
||||
Every subagent's conversation is kept — its brief, what it wrote, every tool call with its output —
|
||||
for as long as the session: **viewable read-only by the person, live while it runs and after it
|
||||
ended**. Nobody types into it; the person speaks to a subagent only through the main agent (or by
|
||||
stopping it). Findings (§8) are kept the same way, readable by its parent, the main agent, and the
|
||||
person.
|
||||
|
||||
## 12. What clients show
|
||||
|
||||
**The tree** — the main agent, every subagent under its parent, open slots, and ended subagents
|
||||
(after the live ones, dimmed, with how each ended):
|
||||
|
||||
| Field | Shown as |
|
||||
|---|---|
|
||||
| `name`, `id`, `position` | `parser` (`s2`) — *reviews the parser* |
|
||||
| `state` | `team.read.states`, its word |
|
||||
| `model` | display name |
|
||||
| `mode` | the mode's name, and request options as `+ write` etc. |
|
||||
| `context` | `{used, window}` → `42%` (a bar where there is room) |
|
||||
| `progress` | its current phase line (`progress.md` §3) |
|
||||
| messages | the room's messages to and from it: how many unread by the person, the last one's first line |
|
||||
|
||||
- **Web UI, Desktop and the CLI** show the whole tree, the room, each subagent's findings and its
|
||||
context (the CLI: `/context <name>`), and open any subagent's transcript read-only, live and after
|
||||
it ended; the person can stop any subagent there.
|
||||
- **Android** shows a reduced list — name, position, state, progress line, context % — and opens a
|
||||
transcript read-only.
|
||||
|
||||
**The data**, the same in `/api/v2` (LLeMbas: a chat's stream carries the tree as it changes) and on
|
||||
the link (`acp.md`, capability `tree`):
|
||||
|
||||
```json
|
||||
{ "min": 1, "max": 4, "size": 2, "open_slots": 0,
|
||||
"agents": [
|
||||
{ "id": "lead", "name": "lead", "position": "the main agent", "parent": null, "state": "working",
|
||||
"model": "qwen-27b", "mode": "edit", "request": [], "manager": true,
|
||||
"context": { "used": 41200, "window": 131072 }, "progress": { "phase": "thinking", "since": 1762790400000 } },
|
||||
{ "id": "s1", "name": "parser", "position": "reviews the parser", "parent": "lead", "state": "working",
|
||||
"model": "qwen-27b", "mode": "manual", "request": ["write"], "manager": false, "task": "…",
|
||||
"context": { "used": 9100, "window": 131072 }, "progress": { "phase": "running_tool", "since": 1762790401000, "tool": "read", "what": "src/parse.ts" },
|
||||
"findings": 2, "messages": 3 }
|
||||
],
|
||||
"ended": [ { "id": "s0", "name": "scout", "position": "…", "parent": "lead", "state": "done", "ended": 1762790300000, "findings": 1 } ] }
|
||||
```
|
||||
|
||||
## 13. What each is told
|
||||
|
||||
- The main agent: `blocks/team_lead.md` (3.4.0: it alone adds; slots; positions; modes and request
|
||||
options; replacing a subagent that ran out of context) and the notes of `team.md` §10.
|
||||
- A subagent: `blocks/team_member.md` (3.4.0: its id and position, `team_whoami`, `team_findings`,
|
||||
`team_finish`, that its context is limited and it will be told when it fills); a sub-manager,
|
||||
`blocks/team_submanager.md` in place of the whole lead block it got in 3.2.0.
|
||||
Reference in new issue
Block a user