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>
236 lines
15 KiB
Markdown
236 lines
15 KiB
Markdown
# 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.
|