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>
This commit is contained in:
14 files changed
+3899
-5
No files matched your search
File diff suppressed because it is too large.
Load diff
+96
-1
@@ -61,6 +61,97 @@
|
||||
},
|
||||
"lifecycle": "A subagent's own background commands stop when it ends, and its report says so. A session's jobs stop when the session is deleted. A front end asked to exit while jobs run asks the person first; one with nobody to ask waits for the jobs the prompt started, then stops the rest."
|
||||
},
|
||||
"team": {
|
||||
"about": "Teams (spec 3.2.0, team.md): a lead and the members it adds, who talk in the team's room. The texts are said word for word; {{size}} is the team's size after the call (team.md §3), {{names}} names joined with \", \" in tree order. conformance/team.json.",
|
||||
"agents": {
|
||||
"about": "The chat setting agents {min, max}: the agents a team may have, the lead included. 1 ≤ min ≤ max ≤ cap. min and max are a new chat's defaults; cap is the instance's (LLeMbas) or the global configuration's (the CLI).",
|
||||
"min": 1,
|
||||
"max": 4,
|
||||
"cap": 32
|
||||
},
|
||||
"names": {
|
||||
"about": "A member's name: unique in the team; `lead` is the lead's, `all` addresses everyone.",
|
||||
"pattern": "^[a-z][a-z0-9-]{0,31}$",
|
||||
"lead": "lead",
|
||||
"reserved": [
|
||||
"all",
|
||||
"lead"
|
||||
]
|
||||
},
|
||||
"marks": {
|
||||
"message": "[Team message from {{from}} to {{to}}:]",
|
||||
"report": "[Team report from {{from}} to {{to}}:]"
|
||||
},
|
||||
"unavailable": {
|
||||
"about": "What a team tool answers while its requirement `team` is not met (team.md §2). one_agent is also each team tool's own `unavailable`, and what task answers while a team is offered, max is 1 and the lead has no member.",
|
||||
"one_agent": "The person allows one agent in this chat: there is no team. Do the work yourself.",
|
||||
"one_request": "This model serves one request at a time now, so there is no team. Do the work yourself."
|
||||
},
|
||||
"work": {
|
||||
"about": "What a manager may not do itself (team.md §6): every call whose risk class is write or execute — an MCP tool's is read when it says readOnlyHint, else execute — but `job_stop`, and a `bash` call that the default rules alone allow in manual mode (reading commands). The tool list is unchanged; these answer texts.manager.",
|
||||
"write": [
|
||||
"apply_patch",
|
||||
"edit",
|
||||
"mkdir",
|
||||
"multiedit",
|
||||
"mv",
|
||||
"rm",
|
||||
"skill_manage",
|
||||
"write"
|
||||
],
|
||||
"execute": [
|
||||
"bash",
|
||||
"branch",
|
||||
"commit",
|
||||
"merge",
|
||||
"pr",
|
||||
"python",
|
||||
"release",
|
||||
"tag"
|
||||
],
|
||||
"kept": [
|
||||
"job_stop"
|
||||
]
|
||||
},
|
||||
"read_max": 50,
|
||||
"texts": {
|
||||
"added": "Added {{name}} ({{role}}): it is working. Its report comes to you when it finishes. The team is {{size}} of {{max}} agents.",
|
||||
"added_waiting": "Added {{name}} ({{role}}): it waits for a free slot — the model serves {{parallel}} requests at once and all are busy — and starts when one frees. Its report comes to you when it finishes. The team is {{size}} of {{max}} agents.",
|
||||
"sent": "Sent to {{to}}.",
|
||||
"stopped": "Stopped {{names}}. The team is {{size}} of {{max}} agents.",
|
||||
"manager": "You lead a team: give this to a member (team_add or team_send).",
|
||||
"full": "The team is full: it has as many agents as the person allows ({{max}}). Give the work to a member already on it (team_send), or stop one (team_stop) first.",
|
||||
"bad_name": "{{name}} cannot be a name: use lowercase letters, digits and hyphens, beginning with a letter, at most 32 characters.",
|
||||
"all_name": "all addresses the whole team; it cannot be a name.",
|
||||
"taken": "There is already an agent called {{name}} in this team. Choose another name.",
|
||||
"no_goal": "Say the team's goal in `goal`, in one line, with your first member: every member is told it.",
|
||||
"unknown": "No agent called {{name}} in this team. The team: {{names}}.",
|
||||
"self": "That is you: send it to someone else.",
|
||||
"nobody": "Nobody else is on the team yet: add a member with team_add.",
|
||||
"not_yours": "{{name}} is not under you: you can stop only the members you lead, directly or through your members.",
|
||||
"nobody_to_stop": "No member is under you: there is nobody to stop.",
|
||||
"no_report": "(no report)",
|
||||
"ended": "[ended: {{reason}}]"
|
||||
},
|
||||
"read": {
|
||||
"about": "team_read's answer: the tree, then the room. Agents in tree order (an agent, then its members in the order they were added, depth first), indented two spaces per level below the lead. `last`: `active now` while working, else how long ago its last turn ended (durations as jobs write them: \"5s ago\", \"1m 05s ago\"). A message's text: its first line after the colon, every further line indented two spaces. Messages above `since` the caller may see, oldest first, at most read_max; `more` when there are others after the last shown.",
|
||||
"tree": "Team ({{size}} of {{max}} agents):",
|
||||
"agent": "{{indent}}- {{name}} — {{role}} · {{state}} · {{model}} · {{last}}",
|
||||
"lead_role": "leads the team",
|
||||
"states": {
|
||||
"working": "working",
|
||||
"waiting": "waiting for a slot",
|
||||
"finished": "finished",
|
||||
"idle": "idle"
|
||||
},
|
||||
"active": "active now",
|
||||
"room": "Room since #{{since}}:",
|
||||
"message": "#{{n}} {{from}} → {{to}}: {{text}}",
|
||||
"report": "#{{n}} {{from}} → {{to}} (report): {{text}}",
|
||||
"none": "No messages since #{{since}}.",
|
||||
"more": "More after #{{last}}: team_read with since {{last}}."
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"about": "Context hygiene (spec 3.0.0, the prefix from 3.1.0; context.md): every measure acts when a result is inserted, and nothing already sent to the model is ever rewritten — changing earlier context throws away the prompt cache from that point. conformance/context.json gives the texts.",
|
||||
"ids": {
|
||||
@@ -112,7 +203,7 @@
|
||||
"started": "Started in: {{path}} (relative to the project root)"
|
||||
},
|
||||
"note": {
|
||||
"about": "A note on the next user message (context.md §10, rule 3): what changed since the system prompt was made, after the person's words. Lines in this order, each only when it applies. `plan_ended` is the mode block for a session that leaves plan mode for a mode with no text of its own (a chat). `plan_own` is the plan block for a session that does not resend earlier tool results (a chat): the model's own plan, as its tools left it. `personal` is the {{path}} of the instructions block for the person's own instructions.",
|
||||
"about": "A note on the next user message (context.md §10, rule 3): what changed since the system prompt was made, after the person's words. Lines in this order, each only when it applies. `plan_ended` is the mode block for a session that leaves plan mode for a mode with no text of its own (a chat). `plan_own` is the plan block for a session that does not resend earlier tool results (a chat): the model's own plan, as its tools left it. `personal` is the {{path}} of the instructions block for the person's own instructions. Spec 3.2.0, where a team is offered (team.md §10): `team` — the size setting, on the first user message and after it changes, followed in the same block by `team_lead` when min ≥ 2 ({{members}} = min − 1), or `team_one` in its place when max is 1; `team_stopped` — members the person stopped, a block of its own.",
|
||||
"open": "<session-note>",
|
||||
"close": "</session-note>",
|
||||
"date": "Today is {{date}}.",
|
||||
@@ -125,6 +216,10 @@
|
||||
"personal": "the person's own instructions",
|
||||
"personality": "The person chose a different personality:\n{{personality}}",
|
||||
"mcp": "Instructions from an MCP server that connected since the session began:\n{{instructions}}",
|
||||
"team": "The team may have {{min}} to {{max}} agents, you included.",
|
||||
"team_lead": "Work through a team from the start: you lead, and members do the work — at least {{members}} of them.",
|
||||
"team_one": "The person allows one agent: work alone; the team tools and task are unavailable.",
|
||||
"team_stopped": "The person stopped {{names}}; they have left the team.",
|
||||
"empty": "(none)"
|
||||
},
|
||||
"mcp_wait_ms": 3000
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
"job_stop": "allow",
|
||||
"job_send": "allow",
|
||||
"report": "allow",
|
||||
"team_add": "allow",
|
||||
"team_send": "allow",
|
||||
"team_read": "allow",
|
||||
"team_stop": "allow",
|
||||
"web_search": "allow",
|
||||
"memory": "allow",
|
||||
"session_search": "allow",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<!-- LLeMbas CLI and LLeMbas, spec 3.2.0 (team.md): how to lead a team, in the system prompt whenever
|
||||
the team tools are offered, and in a member's that may lead. Constant: the size the person
|
||||
allows comes in a session note, never here. -->
|
||||
# Leading a team
|
||||
You can add members to a team with team_add: agents that work in the background, each on the part of the work it owns, while you lead. A small job you may do yourself. Once you have a member you manage and no longer do the work: tools that change things are refused to you, and reading stays yours, for review.
|
||||
- Plan first: what the goal is, what the parts are, and what each one needs from the others. Split by area of ownership, not by step, so that one member owns a thing from start to finish.
|
||||
- Name each member after what it owns. Brief it so that it can work without you: what to do, what done means, what to leave alone, and what to report.
|
||||
- Keep the team flat while it is small — up to six or eight members, each reporting to you. Beyond that, add a lead for each area (`lead: true`), give it the area's goal, and let it build and run its own members; you then lead the leads.
|
||||
- Keep the team within the size the person allows, which a session note tells you. Stop a member whose part is done or no longer needed, to make room.
|
||||
- Coordinate through the room: team_send to the member who owns a thing, `all` only for what everyone needs. team_read shows the team and the room when you need to look; nothing from it reaches you unasked except your members' reports and messages addressed to you.
|
||||
- Step in when a member is stuck, when two disagree, or when the plan has to change: decide, and tell those it concerns.
|
||||
- Review every report against what you asked for — read the work itself where it matters — and send back what is not done. Then give your own answer: to the person, or, if you lead an area, as your report to your manager.
|
||||
@@ -0,0 +1,14 @@
|
||||
<!-- LLeMbas CLI and LLeMbas, spec 3.2.0 (team.md): a team member's standing, said once at the end
|
||||
of its system prompt in place of blocks/subagent.md. The variables are what differs per member;
|
||||
each stands on a line of its own, so no punctuation of theirs matters. -->
|
||||
You are "{{name}}", a member of a team, and you report to "{{manager}}".
|
||||
Your part: {{role}}
|
||||
The team's goal: {{goal}}
|
||||
|
||||
{{instructions}}
|
||||
|
||||
You work for your manager on the part you own, and you cannot ask the person anything: ask your manager, by name, with team_send. Other members own other parts. Message the one who owns a thing when you need it from them, and `all` only for what everyone on the team needs; messages to you arrive as you work. team_read shows who is on the team and the room.
|
||||
|
||||
Your final reply is your report to "{{manager}}" — make it complete on its own: the result, then the evidence (file:line), then anything you could not settle. A message may wake you again afterwards; your next final reply is then your next report.{{read_only}}
|
||||
|
||||
{{lead}}
|
||||
@@ -240,6 +240,27 @@
|
||||
"show": "when the session has a screen (`show` is offered): the sentence saying to open a file the user should see with show — a page, a chart, a document you made — rather than pasting it; otherwise empty",
|
||||
"python": "when `python` is offered: the sentence saying quick calculations and file work can be done in Python in the user's browser, and that it cannot run when no browser page is open; otherwise empty"
|
||||
}
|
||||
},
|
||||
"blocks/team_lead.md": {
|
||||
"scope": "shared",
|
||||
"llembas": [],
|
||||
"llembas_text": "identical",
|
||||
"why": "spec 3.2.0: LLeMbas reads it from the vendored spec (harness_spec.prompt), not from a fragment"
|
||||
},
|
||||
"blocks/team_member.md": {
|
||||
"scope": "shared",
|
||||
"llembas": [],
|
||||
"llembas_text": "identical",
|
||||
"why": "spec 3.2.0: LLeMbas reads it from the vendored spec (harness_spec.prompt), not from a fragment",
|
||||
"variables": {
|
||||
"name": "the member's name in the team",
|
||||
"role": "its role as team_add gave it: one line, what it owns",
|
||||
"manager": "the name of the agent that added it (`lead` for the team's lead)",
|
||||
"goal": "the team's goal as it was when the member was added: the `goal` its team_add gave, else its manager's (team.md §4)",
|
||||
"instructions": "its agent's own instructions (prompts/agents/<name>.md for a built-in one), as in blocks/subagent.md",
|
||||
"read_only": "in plan mode: the sentence saying it can read and search but change nothing (as in blocks/subagent.md); otherwise empty",
|
||||
"lead": "for a member added with `lead: true`: the whole of blocks/team_lead.md; otherwise empty"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "team_add",
|
||||
"scope": "shared",
|
||||
"sessions": "both",
|
||||
"risk": "interact",
|
||||
"exclusive": false,
|
||||
"block": "task",
|
||||
"purpose": false,
|
||||
"requires": [
|
||||
"team"
|
||||
],
|
||||
"unavailable": "The person allows one agent in this chat: there is no team. Do the work yourself.",
|
||||
"aliases": {
|
||||
"cli": [],
|
||||
"llembas": []
|
||||
},
|
||||
"description": "Add a member to your team and start it at once, in the background, on its part of the work. You lead: members do the work while you plan, coordinate and review — once you have a member, tools that change things are refused to you.\n\n- `name` is how everyone addresses it: lowercase letters, digits and hyphens, after what it owns (`parser`, `api-tests`), unique in the team.\n- `role` is one line: what it owns. `task` is its brief and must stand on its own — it sees nothing of this conversation: what to do, when it is done, and what to report.\n- `goal` is the team's goal in a line; your first member needs it, and every member is told it.\n- `lead: true` lets it add members of its own and lead an area; it then manages as you do.\n- The person sets how many agents the team may have; past that this is refused. When the model is already serving as many requests as it can at once, the member waits for a slot and starts when one frees.\n- Its final reply comes to you as its report. Meanwhile team_send messages it, team_read shows the team and the room, team_stop stops it.\n- Agents: {{agents}}",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]{0,31}$",
|
||||
"description": "Its name in the team, after what it owns"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "One line: what it owns"
|
||||
},
|
||||
"task": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Its brief, self-contained"
|
||||
},
|
||||
"goal": {
|
||||
"type": "string",
|
||||
"description": "The team's goal in one line: needed with your first member; a new one is told to members added from now on"
|
||||
},
|
||||
"lead": {
|
||||
"type": "boolean",
|
||||
"description": "May add members of its own and lead an area (default: false)"
|
||||
},
|
||||
"agent": {
|
||||
"type": "string",
|
||||
"description": "Which agent it is (default: general)"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "Run it on another model you may use (from the list in your instructions). Default: yours."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"role",
|
||||
"task"
|
||||
]
|
||||
},
|
||||
"variables": {
|
||||
"agents": "the agents a member may be: `name — description`, separated by \"; \" (as task's)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "team_read",
|
||||
"scope": "shared",
|
||||
"sessions": "both",
|
||||
"risk": "read",
|
||||
"exclusive": false,
|
||||
"block": "tool",
|
||||
"purpose": false,
|
||||
"requires": [
|
||||
"team"
|
||||
],
|
||||
"unavailable": "The person allows one agent in this chat: there is no team. Do the work yourself.",
|
||||
"aliases": {
|
||||
"cli": [],
|
||||
"llembas": []
|
||||
},
|
||||
"description": "The team and its room: every agent (name, role, who leads it, state, model, last activity), and the room's messages after `since` that are yours to see — those you sent or received and those of the members under you; the lead sees them all. Messages are numbered: pass the last number you saw as `since` to read only what is new.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"since": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Show messages after this number (default: 0, from the start)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "team_send",
|
||||
"scope": "shared",
|
||||
"sessions": "both",
|
||||
"risk": "interact",
|
||||
"exclusive": false,
|
||||
"block": "tool",
|
||||
"purpose": false,
|
||||
"requires": [
|
||||
"team"
|
||||
],
|
||||
"unavailable": "The person allows one agent in this chat: there is no team. Do the work yourself.",
|
||||
"aliases": {
|
||||
"cli": [],
|
||||
"llembas": []
|
||||
},
|
||||
"description": "Send a message through your team's room: to one or more agents by name, or to `all`. It reaches each at its next step, or wakes one that has finished, its context intact. Message the agent who owns a thing; use `all` only for what everyone needs. The team's lead is `lead`; your manager is reached by its name.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"to": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"description": "Names, or [\"all\"]"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"to",
|
||||
"message"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "team_stop",
|
||||
"scope": "shared",
|
||||
"sessions": "both",
|
||||
"risk": "interact",
|
||||
"exclusive": false,
|
||||
"block": "tool",
|
||||
"purpose": false,
|
||||
"requires": [
|
||||
"team"
|
||||
],
|
||||
"unavailable": "The person allows one agent in this chat: there is no team. Do the work yourself.",
|
||||
"aliases": {
|
||||
"cli": [],
|
||||
"llembas": []
|
||||
},
|
||||
"description": "Stop a member of your team, and the members under it, where they are: their work stops and they leave the team, which makes room for others. Only members under you, directly or through your members; `all` stops every one of them. Nothing they had not reported comes back — ask for a report with team_send first when you need it.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "A member under you, or all"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
}
|
||||
+8
-3
@@ -48,6 +48,10 @@ import t_skills_list from "../harness/tools/skills_list.json"
|
||||
import t_tag from "../harness/tools/tag.json"
|
||||
import t_task from "../harness/tools/task.json"
|
||||
import t_tasks from "../harness/tools/tasks.json"
|
||||
import t_team_add from "../harness/tools/team_add.json"
|
||||
import t_team_read from "../harness/tools/team_read.json"
|
||||
import t_team_send from "../harness/tools/team_send.json"
|
||||
import t_team_stop from "../harness/tools/team_stop.json"
|
||||
import t_view_image from "../harness/tools/view_image.json"
|
||||
import t_web_fetch from "../harness/tools/web_fetch.json"
|
||||
import t_web_search from "../harness/tools/web_search.json"
|
||||
@@ -78,8 +82,9 @@ export interface ToolSpecDef {
|
||||
/** Spec 3.0.0: what must hold for it to be offered — `display` (the session is shown where a file
|
||||
* can be opened: over the link, to a client that offers `show`), `page`, `forge` (the chat's);
|
||||
* `plan` (3.1.0): plan mode. From 3.1.0 one that can come and go during a session does not
|
||||
* leave the tool out: it is offered and answers `unavailable` while unmet (context.md §10). */
|
||||
requires?: ("display" | "page" | "forge" | "plan")[]
|
||||
* leave the tool out: it is offered and answers `unavailable` while unmet (context.md §10).
|
||||
* `team` (3.2.0): the team setting allows more than one agent (harness/team.md §2). */
|
||||
requires?: ("display" | "page" | "forge" | "plan" | "team")[]
|
||||
/** What a call answers, word for word, when a requirement went away after the offer. */
|
||||
unavailable?: string
|
||||
/** `always`: put to the person in every mode, auto included; never remembered. */
|
||||
@@ -222,7 +227,7 @@ export function narrowParameters(parameters: Record<string, unknown>, narrow: Na
|
||||
}
|
||||
|
||||
export const TOOL_SPECS: Record<string, ToolSpecDef> = Object.fromEntries(
|
||||
([t_apply_patch, t_ask_user, t_bash, t_branch, t_commit, t_decisions, t_edit, t_glob, t_grep, t_job_list, t_job_output, t_job_send, t_job_stop, t_knowledge_get, t_knowledge_search, t_list, t_memory, t_merge, t_mkdir, t_multiedit, t_mv, t_note_manage, t_note_view, t_notes_search, t_plan_submit, t_plan_update, t_plan_write, t_pr, t_python, t_read, t_recall, t_release, t_report, t_rm, t_search, t_session_search, t_settings, t_show, t_skill_manage, t_skill_view, t_skills_list, t_tag, t_task, t_tasks, t_view_image, t_web_fetch, t_web_search, t_write] as ToolSpecDef[]).map((t) => [t.name, t]),
|
||||
([t_apply_patch, t_ask_user, t_bash, t_branch, t_commit, t_decisions, t_edit, t_glob, t_grep, t_job_list, t_job_output, t_job_send, t_job_stop, t_knowledge_get, t_knowledge_search, t_list, t_memory, t_merge, t_mkdir, t_multiedit, t_mv, t_note_manage, t_note_view, t_notes_search, t_plan_submit, t_plan_update, t_plan_write, t_pr, t_python, t_read, t_recall, t_release, t_report, t_rm, t_search, t_session_search, t_settings, t_show, t_skill_manage, t_skill_view, t_skills_list, t_tag, t_task, t_tasks, t_team_add, t_team_read, t_team_send, t_team_stop, t_view_image, t_web_fetch, t_web_search, t_write] as ToolSpecDef[]).map((t) => [t.name, t]),
|
||||
)
|
||||
|
||||
/** Whether the spec gives a tool to this CLI: not the server's alone (`scope: llembas`), and
|
||||
|
||||
@@ -17,6 +17,8 @@ import blockLibrary from "../../harness/prompts/blocks/library.md" with { type:
|
||||
import blockPrototypes from "../../harness/prompts/blocks/prototypes.md" with { type: "text" }
|
||||
import blockSubagent from "../../harness/prompts/blocks/subagent.md" with { type: "text" }
|
||||
import blockContext from "../../harness/prompts/blocks/context.md" with { type: "text" }
|
||||
import blockTeamLead from "../../harness/prompts/blocks/team_lead.md" with { type: "text" }
|
||||
import blockTeamMember from "../../harness/prompts/blocks/team_member.md" with { type: "text" }
|
||||
import agentExplore from "../../harness/prompts/agents/explore.md" with { type: "text" }
|
||||
import agentGeneral from "../../harness/prompts/agents/general.md" with { type: "text" }
|
||||
import modeManual from "../../harness/prompts/modes/manual.md" with { type: "text" }
|
||||
@@ -56,6 +58,8 @@ const BUILTIN: Record<string, string> = {
|
||||
"blocks/prototypes.md": blockPrototypes,
|
||||
"blocks/subagent.md": blockSubagent,
|
||||
"blocks/context.md": blockContext,
|
||||
"blocks/team_lead.md": blockTeamLead,
|
||||
"blocks/team_member.md": blockTeamMember,
|
||||
"agents/explore.md": agentExplore,
|
||||
"agents/general.md": agentGeneral,
|
||||
"modes/manual.md": modeManual,
|
||||
|
||||
+13
-1
@@ -153,10 +153,22 @@ test("spec 3.0.0: every tool says where it is offered; the CLI's are the agent's
|
||||
// plan_write's former name rather than a tool.
|
||||
expect(TOOL_SPECS.todo).toBeUndefined()
|
||||
expect(TOOL_SPECS.plan_write!.aliases.cli).toEqual(["todo"])
|
||||
const shared = ["ask_user", "edit", "glob", "grep", "job_list", "job_output", "job_send", "job_stop", "list", "memory", "plan_submit", "plan_update", "plan_write", "read", "recall", "report", "session_search", "show", "skill_manage", "skill_view", "skills_list", "task", "web_fetch", "web_search", "write"]
|
||||
const shared = ["ask_user", "edit", "glob", "grep", "job_list", "job_output", "job_send", "job_stop", "list", "memory", "plan_submit", "plan_update", "plan_write", "read", "recall", "report", "session_search", "show", "skill_manage", "skill_view", "skills_list", "task", "team_add", "team_read", "team_send", "team_stop", "web_fetch", "web_search", "write"]
|
||||
expect(Object.values(TOOL_SPECS).filter((t) => t.scope === "shared").map((t) => t.name).sort()).toEqual(shared.sort())
|
||||
})
|
||||
|
||||
test("spec 3.2.0: the team tools need `team` and answer one_agent; a manager's refused tools are exactly the writers and executors", () => {
|
||||
const loop = JSON.parse(readFileSync(join(HARNESS, "loop.json"), "utf8")) as { team: { unavailable: { one_agent: string }; work: { write: string[]; execute: string[]; kept: string[] } } }
|
||||
for (const n of ["team_add", "team_read", "team_send", "team_stop"]) {
|
||||
const t = TOOL_SPECS[n]!
|
||||
expect([t.scope, t.sessions, t.requires, t.unavailable]).toEqual(["shared", "both", ["team"], loop.team.unavailable.one_agent])
|
||||
}
|
||||
// team.work names every tool by its risk class, and nothing else: a new tool is placed by its class.
|
||||
const byRisk = (r: string) => Object.values(TOOL_SPECS).filter((t) => t.risk === r).map((t) => t.name).sort()
|
||||
expect([...loop.team.work.write].sort()).toEqual(byRisk("write"))
|
||||
expect([...loop.team.work.execute, ...loop.team.work.kept].sort()).toEqual(byRisk("execute"))
|
||||
})
|
||||
|
||||
test("the personality presets are shared prompt texts, one paragraph each", () => {
|
||||
const index = JSON.parse(readFileSync(join(HARNESS, "prompts", "index.json"), "utf8")) as { prompts: Record<string, { scope: string; llembas_text?: string }> }
|
||||
for (const n of ["concise", "pragmatic", "optimistic", "funny", "formal", "socratic"]) {
|
||||
|
||||
Reference in new issue
Block a user