Files
LLeMbas-CLI/harness/team.md
T
HomerandClaude Opus 5.5 880289970b Harness spec 3.2.0: teams — subagents that work together
team.md: a lead adds members (team_add) that work in the background on
the parts they own; members may lead areas of their own. The chat
setting agents {min, max} (defaults 1/4, an instance cap of 32) bounds
the whole tree, task helpers included; a team is offered only where
subagents are enabled and the model serves two requests at once
(capacity.parallel). A manager's work calls — risk write or execute,
bar job_stop and reading bash — answer a fixed refusal while the tool
list stays unchanged. The room: team_send to names or all, delivered
under "[Team message from X to Y:]" at the next step or waking a
finished member; a member's final reply is its report to its manager;
team_read shows the tree and the visible part of the room; team_stop
stops a subtree. Past the model's capacity a member waits for a slot,
the lead first. Size notes in context.note; texts in loop.json team;
prompts blocks/team_lead.md and team_member.md; tools allowed by
default. Conformance area team, written from a reference model.

The CLI embeds the new tool and prompt files; its tools are pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:17:48 +00:00

236 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Teams
Spec 3.2.0. **Subagents that work together.** A session's agent — the **lead** — may add
**members**, background agents that each own a part of the work, and lead them as a manager: it
plans, splits the work, coordinates and reviews, and the members do it. Members talk to each other
and to their managers in the team's **room**. A member may itself lead an area with members of its
own, so a team grows from a few agents side by side into a small company with one goal. The same in
a chat on the server and in an agent session on a device, in both projects.
The tools are `tools/team_add.json`, `team_send.json`, `team_read.json`, `team_stop.json`; the texts
and numbers `loop.json` `team` (and the size notes in `context.note`); the prompt texts
`prompts/blocks/team_lead.md` and `team_member.md`; the cases `conformance/team.json`.
## 1. Words
- **Agent**: the lead or a member. Every agent has a **name**: the lead is `lead`; a member the name
it was added under. Names are unique in the team.
- **Member**: an agent added with `team_add`, by the lead or by a member that leads. Its **manager**
is the agent that added it. The members an agent added, and theirs, down the tree, are **under**
it; with the agent itself they are its **subtree**.
- **Manager**: an agent with at least one member, and the lead from the start when the team's `min`
is 2 or more (§2). A manager does not do the work (§6).
- **Team**: the lead and every member, one per lead session; **its size** is how many agents it has
now (§3).
- **Room**: the team's messages, numbered from 1 in the order they were posted, each with `n`, `from`
(a name), `to` (as addressed: names, or `all`), its recipients (resolved when it was posted), the
text, the time, and whether it is a report.
## 2. The size setting, and when a team is offered
A chat has the setting **`agents: {min, max}`** — the agents the team may have, the lead included.
- `1 ≤ min ≤ max ≤ cap`. `cap` is the instance's (LLeMbas) or the global configuration's (the CLI),
default `loop.json` `team.agents.cap` (32); a project's configuration never raises it. A value
outside is refused where it is set, with what the bounds are, never clamped silently; a stored
setting the cap was lowered under is read as `max = min(max, cap)`, `min = min(min, max)`.
- Defaults for a new chat: `min` 1, `max` 4 (`team.agents`) — the lead and up to three members. LLeMbas
keeps a per-person default for new chats, seeded with those; the CLI reads them from its
configuration.
- The setting is kept with the chat. A change during a session is told to the lead as a note on its
next user message (`context.md` §10; `context.note.team`, below), never by changing the system
prompt.
**A team is offered** — the setting shown, the team tools listed, `blocks/team_lead.md` in the
system prompt — when both hold, and only then:
1. Subagents are enabled for the session: LLeMbas — the instance allows them and the chat's model
has its subagent tool; the CLI — its subagents are not switched off in the configuration.
2. **The chat's model serves at least two requests at once**: `capacity.parallel ≥ 2`, or neither
`parallel` nor `single_session` is set (a hosted API; `models/schema.json`).
Both are fixed for as long as the system prompt is (`context.md` §10, rule 4: a model switch makes
a new prompt and a new list). Otherwise the setting is not shown and none of the four tools is
listed, and the session's `task` works as before.
While the tools are listed they need **`team`** (`requires`): met when `max ≥ 2` and the model still
serves two at once — or, for the lead, when its team still has a member (so a lead whose person
lowered `max` to 1 can still talk to, read and stop the members it has). A call while it is not
met answers, word for word, `team.unavailable.one_request` when the model's capacity no longer
allows a team (its metadata changed after the prompt was made), else `team.unavailable.one_agent`
— which is also the tools' own `unavailable` text.
The meaning of the setting:
- **`max = 1`**: one agent, no team, and no helper of any kind: `task` answers `one_agent` too
(while the lead has no member; with members left from a larger setting, `full`).
- **`min ≥ 2`**: the lead works through a team from the start — it is a manager before its first
member (§6), and the note asks for at least `min − 1` members.
- **`min = 1`, `max ≥ 2`**: the lead decides; a small job it may do alone. Once it has a member it is
a manager.
## 3. Size, and what counts
**The size** of a team is: the lead, every member (whatever its state — a finished member can be
woken by a message, so it keeps its place), and every `task` helper running in the team, started by
any agent in it (the CLI's background `task` jobs and foreground ones alike, while they run).
A stopped member does not count: stopping is how room is made.
- **`team_add` is refused past `max`** — when the size would become more than `max`, with
`team.texts.full`. So is `task`, while a team is offered: with `full`, or with `one_agent` when
`max` is 1 and the lead has no member. A `job_send` that would start a finished background
`task` helper again is refused the same way.
- **Lowering `max` stops nobody.** Members already there stay; nothing new is added until the size
is under the new `max`.
- The size is across **the whole tree**: a member that leads adds within the same `max` as the lead.
## 4. Members
- `team_add` starts the member **at once, in the background**, under the caller: the existing job
machinery (a hidden chat on LLeMbas with its `chat_jobs` row, a subagent job in the CLI), running
unattended — no `ask_user`, the chat's own permission ceiling, its mode no looser than its
manager's (`permission/modes.json`).
- **Its agent** is `general` (the default: it does the work) or `explore` (read-only); `model` is
as `task`'s — a model the person may use, the default being the caller's.
- **Its tools** are those its agent gives a subagent, plus `team_send` and `team_read`; one added
with `lead: true` also has `team_add` and `team_stop`. They are fixed when it is made (§10 rule 4:
a subagent's narrowing). `report` (`offered: subagent`) sends its manager a message before its
final reply: posted in the room to the manager, delivered with the message mark.
- **Its system prompt** is built once, when it is added: what a subagent of that agent gets, with
`blocks/team_member.md` in place of `blocks/subagent.md` — its name, role, manager, the team's
goal, the agent's own instructions, and for one with `lead: true` the whole of
`blocks/team_lead.md`. Its first user message is its brief (`task`), as given.
- **The team's goal** is what a member is told the team is for: the `goal` the caller gave in
`team_add`, else the caller's own goal. The lead's own goal is the last `goal` it gave; **the
lead's first `team_add` must give one** (`team.texts.no_goal`). A member already working keeps the
goal it was told; a new goal reaches it only as a message.
- **`team_add`'s refusals are checked in this order**, the first that applies answering:
`unavailable` (§2); `all_name` (the name `all`); `bad_name` (not `team.names.pattern`); `taken`
(the name of an agent on the team, `lead` included); `no_goal`; `full`. Then the member is added:
`added`, or `added_waiting` when it has to wait for a slot (§8). An unknown `agent` or `model`
answers as `task` does.
- **Members are not jobs to the model.** `job_list` does not show them and the `job_*` tools do not
take them; the team tools do. (An implementation may run them on its job machinery.)
- **States**: `working` (a turn is running), `waiting` (it has work, and waits for a slot, §8),
`finished` (its last turn ended; it can be woken). The lead is `working`, `waiting` or `idle`.
- A member's background commands stop when its turn ends (`loop.json` `jobs.lifecycle`). Deleting
the lead's session stops the whole team.
## 5. The room: who receives what
- **`team_send`** posts one message: `to` a list of names, or `["all"]` (a lone string is read as a
one-name list). Names are deduplicated, kept in the order given. `all` anywhere in the list means
all, and the other names in it are ignored.
- **Recipients**: for named agents, exactly those named; for `all`, every agent of the team but the
sender — the lead and every member, whoever manages them (the room is one channel for the whole
company). Resolved when the message is posted: an agent added later does not get it.
- **Refused, and nothing posted**, checked in this order: `all` when nobody else is on the team
(`nobody`); a name that is not on the team (`unknown`, the first such name in the order given);
the sender's own name among them (`self`). The answer when
posted: `team.texts.sent`, `to` as addressed.
- **What an agent receives** is only what is addressed to it, by name or as `all`, and — a manager
— **the reports of its own members**. A manager is not sent its members' messages to each other;
it reads them with `team_read`, on demand, so nothing floods its context.
- **A report**: a member's final reply, when its turn ends, is posted to its manager (`report`
true) and delivered to it. An empty reply reads `team.texts.no_report`; a turn that ended
otherwise than by the model stopping adds `team.texts.ended` (`steps`, `budget`, `error — <text>`),
after a blank line. A sub-manager's final reply is likewise its report to its manager.
- **The person's** messages go to the lead as its user messages, not into the room; the lead's
answers to the person are not room messages either. The person sees the whole room and the tree
in the UI (LLeMbas: the Team panel; the CLI: `/team`), and can stop a member there — said to the
lead as a note (`context.note.team_stopped`).
- **`team_read`** answers the tree — every agent, the whole team, in tree order: name, role, its
manager, state, model, last activity — and the room's messages numbered above `since` (default 0)
that the caller may see, oldest first, at most `team.read_max` (50), and how to read on. **Who may
see a message**: the lead every one; any other agent those whose sender or one of whose recipients
is in its subtree — for a member that leads nobody, what it sent and what it received.
## 6. A manager does not do the work
A manager's **work** calls are refused, before the permission check — nothing is asked — with
`team.texts.manager`, word for word:
> You lead a team: give this to a member (team_add or team_send).
**Work** is a call whose risk class (`risk`; an MCP tool's: `read` when it says `readOnlyHint`, else
`execute`) is `write` or `execute`, with two exceptions that stay a manager's:
- `job_stop` — managing what runs is not work;
- a `bash` call **the default rules alone allow in manual mode** (`permission/defaults.json`, the
`permission` conformance area's evaluation with no rules of the person's): reading commands —
`git status`, `git diff`, `cat`, `ls`, `grep` — for review.
So with the spec's tools: **refused** — `write`, `edit`, `multiedit`, `apply_patch`, `mkdir`, `mv`,
`rm`, `skill_manage` (write); `bash` beyond reading, `python`, `commit`, `branch`, `pr`, `merge`,
`tag`, `release` (execute); an MCP tool that does not say it only reads. **Kept** — every `read`
tool (`read`, `glob`, `grep`, `list`, `search`, `recall`, `web_search`, `web_fetch`, …), every
`interact` tool (`ask_user` for the lead, `plan_*`, `task`, `memory`, `show`, `job_send`, `report`,
the team tools, …) and `job_stop`. `team.work` lists the same, and a new tool is placed by its risk
class.
**The tool list does not change** when an agent becomes a manager (`context.md` §10, rule 4): the
same tools are listed and the refused ones answer the refusal. An agent is a manager while it has
a member — a finished one included; one whose members were all stopped is not, unless it is the
lead and `min ≥ 2` with the team offered. `task` stays: a one-off helper is delegation, and counts
toward the size (§3).
## 7. Delivery, and waking
A message or a report reaches its recipient **as a marked message** — the jobs mechanism
(`loop.json` `jobs`): while the recipient's turn runs, as a user message at its next step boundary
(which does not start a turn: `context.md` §8); otherwise it **wakes** the recipient — a finished
member, or the idle lead — as a new turn of its own, marked `machine`, with its context intact, as
`job_send` restarts a finished helper. A woken agent needs a slot (§8). Several waiting at one step
boundary go in one user message, in room order, separated by a blank line.
Every one begins with its mark, on a line of its own, then the text:
```
[Team message from parser to lead:]
The grammar is done; the tests are next.
```
- message: `[Team message from {{from}} to {{to}}:]` — `to` as addressed: the names joined with
`, `, or `all`; the same for every recipient.
- report: `[Team report from {{from}} to {{to}}:]` — `to` the manager.
A session that is not open keeps them until it is resumed, as a job's note.
## 8. Capacity: a member waits for a slot
A model serves `parallel` requests at once (`models/schema.json`). **An agent holds a slot of its
model while its turn runs** — the lead, a member, a `task` helper. An agent with work (added, woken,
given a message while finished, the lead given the person's message) starts when its model has a
free slot and none of its `capacity.group`'s other models is busy; otherwise it is `waiting`.
`team_add` is never refused for capacity (only for size): its answer says the member waits.
Waiting agents start **in order**: the lead first (the person waits on it), then the others in the
order they began waiting. A slot frees when a turn ends or an agent is stopped. `task` keeps its own
rule (`loop.json` `capacity`: refused where its model is full), and its helper holds a slot.
## 9. Stopping
`team_stop` stops one member **and its whole subtree**, or with `all` every member under the caller:
their turns stop where they are, their background commands with them, messages waiting for them
are dropped, and they **leave the team** — the size drops, their slots free, their names may be
used again. No report comes back from a stopped member. Refused, in this order: `all` with nobody
under the caller (`nobody_to_stop`); a name not on the team (`unknown`); the caller itself or an
agent not under it (`not_yours`). The answer, `stopped`, names the stopped in tree order.
## 10. What the lead is told
- `blocks/team_lead.md` — constant, no numbers — is in the system prompt whenever the team tools
are listed: how to lead.
- The size is in a **note** (`context.md` §10, `context.note`): on the first user message of the
session, and on the next one after the setting changed — `team` (`min`, `max`), with `team_lead`
after it when `min ≥ 2` (`members` = `min − 1`); when `max` is 1, `team_one` instead. One block.
A member the person stopped is its own block, `team_stopped`, with the names. Members are told
nothing of the size: `team_add`'s answer says how big the team is.
## 11. Over the link
An agent chat's team runs on the device, as its jobs do. Spec 3.2.0 does not carry the room over
ACP yet: a device with the `jobs` capability reports each member as a subagent job (`job` events,
`description` being `<name>: <role>`), so the client's job strip shows them, and the room is shown
on the device (`/team`). Carrying the room is a later spec's.