diff --git a/harness/conformance/team.json b/harness/conformance/team.json new file mode 100644 index 0000000..052b666 --- /dev/null +++ b/harness/conformance/team.json @@ -0,0 +1,3332 @@ +{ + "about": "Teams (team.md, spec 3.2.0): one lead session's team, step by step, written from a reference model of the rules. `start`: the size setting `min` and `max`, `subagents` (enabled for the session; default true) and the chat model's `capacity` (models/schema.json: `parallel`, `single_session`; {} — no limit). The lead is `lead`; its first prompt is running as the case begins, and every agent is on the chat's model. Steps and what each answers: `offered` (`by` an agent, default lead) — `{tools, setting}`: the team tools listed for that agent, sorted, and whether the setting is shown; `call` (`by`, `tool`, `args`) — `{result, delivered}`: the call's answer word for word (null where an ordinary tool would run: the case looks only at whether it is refused), and what was delivered to whom by this step, `{name: [message, …]}`; `team_read` answers `{room, delivered}` instead, `room` being the messages it shows as `{n, from, to}` (the tree and the text format are loop.json team.read's, checked by each project's own tests); `task` runs a helper that counts toward the size (and holds a slot) until `helper_done` (`by`) — answers null; `finish` (`by`, `reply`, `reason`: stop by default, or steps, budget, `error — `) — the agent's turn ends: a member's reply is its report — `{delivered}`; `set` (`min`, `max`, `capacity`: the person's setting or the model's metadata changed) — null; `person_stop` (`name`) — the person stopped that member in the UI — null; `user` — the person's message to the lead — `{note}`: the note on it (context.md §10), null when there is none; `state` — the team in tree order as `{name, manager, state}`: working, waiting (for a slot), finished, idle (the lead between turns). Delivering to a finished member or the idle lead wakes it (it is then working, or waiting when its model is full). LLeMbas: services/team.py; CLI: team/.", + "cases": [ + { + "about": "Offered on a model that serves two requests at once: the lead has all four tools and the setting is shown.", + "start": { + "min": 1, + "max": 4, + "capacity": { + "parallel": 2 + } + }, + "steps": [ + { + "op": "offered" + } + ], + "expect": [ + { + "tools": [ + "team_add", + "team_read", + "team_send", + "team_stop" + ], + "setting": true + } + ] + }, + { + "about": "A hosted API (no capacity said) offers a team too.", + "start": { + "min": 1, + "max": 4, + "capacity": {} + }, + "steps": [ + { + "op": "offered" + } + ], + "expect": [ + { + "tools": [ + "team_add", + "team_read", + "team_send", + "team_stop" + ], + "setting": true + } + ] + }, + { + "about": "A model that serves one request at a time offers no team: no tools, no setting.", + "start": { + "min": 1, + "max": 4, + "capacity": { + "single_session": true + } + }, + "steps": [ + { + "op": "offered" + } + ], + "expect": [ + { + "tools": [], + "setting": false + } + ] + }, + { + "about": "parallel 1 is the same as single_session.", + "start": { + "min": 1, + "max": 4, + "capacity": { + "parallel": 1, + "single_session": false + } + }, + "steps": [ + { + "op": "offered" + } + ], + "expect": [ + { + "tools": [], + "setting": false + } + ] + }, + { + "about": "parallel wins over single_session where they disagree.", + "start": { + "min": 1, + "max": 4, + "capacity": { + "parallel": 3, + "single_session": true + } + }, + "steps": [ + { + "op": "offered" + } + ], + "expect": [ + { + "tools": [ + "team_add", + "team_read", + "team_send", + "team_stop" + ], + "setting": true + } + ] + }, + { + "about": "Subagents switched off: no team.", + "start": { + "min": 1, + "max": 4, + "subagents": false, + "capacity": { + "parallel": 4 + } + }, + "steps": [ + { + "op": "offered" + } + ], + "expect": [ + { + "tools": [], + "setting": false + } + ] + }, + { + "about": "max 1: the tools stay listed (the setting can change during the session) and answer that one agent is allowed; task answers the same.", + "start": { + "min": 1, + "max": 1, + "capacity": { + "parallel": 2 + } + }, + "steps": [ + { + "op": "offered" + }, + { + "op": "user" + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "A parser" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_send", + "args": { + "to": [ + "all" + ], + "message": "hi" + } + }, + { + "op": "call", + "by": "lead", + "tool": "task", + "args": { + "prompt": "x" + } + }, + { + "op": "call", + "by": "lead", + "tool": "write", + "args": {} + } + ], + "expect": [ + { + "tools": [ + "team_add", + "team_read", + "team_send", + "team_stop" + ], + "setting": true + }, + { + "note": "\nThe person allows one agent: work alone; the team tools and task are unavailable.\n" + }, + { + "result": "The person allows one agent in this chat: there is no team. Do the work yourself.", + "delivered": {} + }, + { + "result": "The person allows one agent in this chat: there is no team. Do the work yourself.", + "delivered": {} + }, + { + "result": "The person allows one agent in this chat: there is no team. Do the work yourself.", + "delivered": {} + }, + { + "result": null, + "delivered": {} + } + ] + }, + { + "about": "A member that may not lead has team_send and team_read; one that may lead has all four.", + "start": { + "min": 1, + "max": 4, + "capacity": { + "parallel": 4 + } + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "Ship the parser" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "docs", + "role": "owns a part", + "task": "Do the part.", + "lead": true + } + }, + { + "op": "offered", + "by": "parser" + }, + { + "op": "offered", + "by": "docs" + } + ], + "expect": [ + { + "result": "Added parser (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 4 agents.", + "delivered": {} + }, + { + "result": "Added docs (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 4 agents.", + "delivered": {} + }, + { + "tools": [ + "team_read", + "team_send" + ], + "setting": true + }, + { + "tools": [ + "team_add", + "team_read", + "team_send", + "team_stop" + ], + "setting": true + } + ] + }, + { + "about": "The size is a note on the first user message, and on the next one after it changes — never in the prompt.", + "start": { + "min": 1, + "max": 4, + "capacity": { + "parallel": 2 + } + }, + "steps": [ + { + "op": "user" + }, + { + "op": "finish", + "by": "lead" + }, + { + "op": "user" + }, + { + "op": "set", + "max": 6 + }, + { + "op": "finish", + "by": "lead" + }, + { + "op": "user" + }, + { + "op": "set", + "max": 6 + }, + { + "op": "finish", + "by": "lead" + }, + { + "op": "user" + } + ], + "expect": [ + { + "note": "\nThe team may have 1 to 4 agents, you included.\n" + }, + { + "delivered": {} + }, + { + "note": null + }, + null, + { + "delivered": {} + }, + { + "note": "\nThe team may have 1 to 6 agents, you included.\n" + }, + null, + { + "delivered": {} + }, + { + "note": null + } + ] + }, + { + "about": "min 2 or more: the note asks for a team from the start, and the lead is a manager before its first member.", + "start": { + "min": 3, + "max": 5, + "capacity": { + "parallel": 4 + } + }, + "steps": [ + { + "op": "user" + }, + { + "op": "call", + "by": "lead", + "tool": "edit", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "read", + "args": {} + } + ], + "expect": [ + { + "note": "\nThe team may have 3 to 5 agents, you included.\nWork through a team from the start: you lead, and members do the work — at least 2 of them.\n" + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": null, + "delivered": {} + } + ] + }, + { + "about": "Lowering max to 1 is said as one agent.", + "start": { + "min": 1, + "max": 4, + "capacity": { + "parallel": 2 + } + }, + "steps": [ + { + "op": "user" + }, + { + "op": "set", + "max": 1 + }, + { + "op": "finish", + "by": "lead" + }, + { + "op": "user" + } + ], + "expect": [ + { + "note": "\nThe team may have 1 to 4 agents, you included.\n" + }, + null, + { + "delivered": {} + }, + { + "note": "\nThe person allows one agent: work alone; the team tools and task are unavailable.\n" + } + ] + }, + { + "about": "min equal to max: the range and the request for a team.", + "start": { + "min": 2, + "max": 2, + "capacity": { + "parallel": 2 + } + }, + "steps": [ + { + "op": "user" + } + ], + "expect": [ + { + "note": "\nThe team may have 2 to 2 agents, you included.\nWork through a team from the start: you lead, and members do the work — at least 1 of them.\n" + } + ] + }, + { + "about": "Past max, team_add is refused; a stopped member makes room. The size counts the lead.", + "start": { + "min": 1, + "max": 3, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "Ship the parser" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "lexer", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "tests", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_stop", + "args": { + "name": "lexer" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "tests", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "state" + } + ], + "expect": [ + { + "result": "Added parser (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 3 agents.", + "delivered": {} + }, + { + "result": "Added lexer (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 3 agents.", + "delivered": {} + }, + { + "result": "The team is full: it has as many agents as the person allows (3). Give the work to a member already on it (team_send), or stop one (team_stop) first.", + "delivered": {} + }, + { + "result": "Stopped lexer. The team is 2 of 3 agents.", + "delivered": {} + }, + { + "result": "Added tests (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 3 agents.", + "delivered": {} + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "parser", + "manager": "lead", + "state": "working" + }, + { + "name": "tests", + "manager": "lead", + "state": "working" + } + ] + ] + }, + { + "about": "A finished member keeps its place in the size: it can be woken.", + "start": { + "min": 1, + "max": 2, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "Ship the parser" + } + }, + { + "op": "finish", + "by": "parser", + "reply": "Done." + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "lexer", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "state" + } + ], + "expect": [ + { + "result": "Added parser (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 2 agents.", + "delivered": {} + }, + { + "delivered": { + "lead": [ + "[Team report from parser to lead:]\nDone." + ] + } + }, + { + "result": "The team is full: it has as many agents as the person allows (2). Give the work to a member already on it (team_send), or stop one (team_stop) first.", + "delivered": {} + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "parser", + "manager": "lead", + "state": "finished" + } + ] + ] + }, + { + "about": "The size is across the whole tree: a member that leads adds within the same max.", + "start": { + "min": 1, + "max": 4, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "backend", + "role": "owns a part", + "task": "Do the part.", + "lead": true, + "goal": "Ship v2" + } + }, + { + "op": "call", + "by": "backend", + "tool": "team_add", + "args": { + "name": "db", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "frontend", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "backend", + "tool": "team_add", + "args": { + "name": "api", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "state" + } + ], + "expect": [ + { + "result": "Added backend (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 4 agents.", + "delivered": {} + }, + { + "result": "Added db (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 4 agents.", + "delivered": {} + }, + { + "result": "Added frontend (owns a part): it is working. Its report comes to you when it finishes. The team is 4 of 4 agents.", + "delivered": {} + }, + { + "result": "The team is full: it has as many agents as the person allows (4). Give the work to a member already on it (team_send), or stop one (team_stop) first.", + "delivered": {} + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "backend", + "manager": "lead", + "state": "working" + }, + { + "name": "db", + "manager": "backend", + "state": "working" + }, + { + "name": "frontend", + "manager": "lead", + "state": "working" + } + ] + ] + }, + { + "about": "A task helper counts toward max while it runs, inside a team.", + "start": { + "min": 1, + "max": 3, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "Ship the parser" + } + }, + { + "op": "call", + "by": "lead", + "tool": "task", + "args": { + "prompt": "Find the grammar." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "lexer", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "parser", + "tool": "task", + "args": { + "prompt": "x" + } + }, + { + "op": "helper_done", + "by": "lead" + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "lexer", + "role": "owns a part", + "task": "Do the part." + } + } + ], + "expect": [ + { + "result": "Added parser (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 3 agents.", + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": "The team is full: it has as many agents as the person allows (3). Give the work to a member already on it (team_send), or stop one (team_stop) first.", + "delivered": {} + }, + { + "result": "The team is full: it has as many agents as the person allows (3). Give the work to a member already on it (team_send), or stop one (team_stop) first.", + "delivered": {} + }, + null, + { + "result": "Added lexer (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 3 agents.", + "delivered": {} + } + ] + }, + { + "about": "Lowering max stops nobody: the members stay, and nothing is added until the size is under it. The lead keeps its team tools while it has members.", + "start": { + "min": 1, + "max": 4, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "a", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "b", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "set", + "max": 1 + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "c", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_send", + "args": { + "to": [ + "a" + ], + "message": "Wrap up." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_stop", + "args": { + "name": "all" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_send", + "args": { + "to": [ + "all" + ], + "message": "x" + } + }, + { + "op": "state" + } + ], + "expect": [ + { + "result": "Added a (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 4 agents.", + "delivered": {} + }, + { + "result": "Added b (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 4 agents.", + "delivered": {} + }, + null, + { + "result": "The team is full: it has as many agents as the person allows (1). Give the work to a member already on it (team_send), or stop one (team_stop) first.", + "delivered": {} + }, + { + "result": "Sent to a.", + "delivered": { + "a": [ + "[Team message from lead to a:]\nWrap up." + ] + } + }, + { + "result": "Stopped a, b. The team is 1 of 1 agents.", + "delivered": {} + }, + { + "result": "The person allows one agent in this chat: there is no team. Do the work yourself.", + "delivered": {} + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + } + ] + ] + }, + { + "about": "The model's capacity changed after the prompt was made: the tools answer one_request.", + "start": { + "min": 1, + "max": 4, + "capacity": { + "parallel": 2 + } + }, + "steps": [ + { + "op": "set", + "capacity": { + "single_session": true + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + } + ], + "expect": [ + null, + { + "result": "This model serves one request at a time now, so there is no team. Do the work yourself.", + "delivered": {} + } + ] + }, + { + "about": "Names: the pattern, all, lead and a name already on the team are refused; the first member needs the goal.", + "start": { + "min": 1, + "max": 8, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "Parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "all", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "lead", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "9lives", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "role": "owns a part", + "task": "Do the part." + } + } + ], + "expect": [ + { + "result": "Parser cannot be a name: use lowercase letters, digits and hyphens, beginning with a letter, at most 32 characters.", + "delivered": {} + }, + { + "result": "all addresses the whole team; it cannot be a name.", + "delivered": {} + }, + { + "result": "There is already an agent called lead in this team. Choose another name.", + "delivered": {} + }, + { + "result": "Say the team's goal in `goal`, in one line, with your first member: every member is told it.", + "delivered": {} + }, + { + "result": "Added parser (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 8 agents.", + "delivered": {} + }, + { + "result": "There is already an agent called parser in this team. Choose another name.", + "delivered": {} + }, + { + "result": "9lives cannot be a name: use lowercase letters, digits and hyphens, beginning with a letter, at most 32 characters.", + "delivered": {} + }, + { + "result": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa cannot be a name: use lowercase letters, digits and hyphens, beginning with a letter, at most 32 characters.", + "delivered": {} + }, + { + "result": "Added aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 8 agents.", + "delivered": {} + } + ] + }, + { + "about": "Once it has a member, the lead does not do the work: writers and executors are refused, readers and the interact tools stay, and bash only for what the default rules allow.", + "start": { + "min": 1, + "max": 4, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "write", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "Ship the parser" + } + }, + { + "op": "call", + "by": "lead", + "tool": "write", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "edit", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "multiedit", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "apply_patch", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "mkdir", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "mv", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "rm", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "skill_manage", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "python", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "commit", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "branch", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "pr", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "merge", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "tag", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "release", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "bash", + "args": { + "command": "git status" + } + }, + { + "op": "call", + "by": "lead", + "tool": "bash", + "args": { + "command": "cat README.md" + } + }, + { + "op": "call", + "by": "lead", + "tool": "bash", + "args": { + "command": "grep -r foo ." + } + }, + { + "op": "call", + "by": "lead", + "tool": "bash", + "args": { + "command": "npm test" + } + }, + { + "op": "call", + "by": "lead", + "tool": "bash", + "args": { + "command": "ls && rm -rf build" + } + }, + { + "op": "call", + "by": "lead", + "tool": "read", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "glob", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "grep", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "list", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "recall", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "web_fetch", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "ask_user", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "plan_write", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "memory", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "show", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "job_send", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "job_stop", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "task", + "args": {} + } + ], + "expect": [ + { + "result": null, + "delivered": {} + }, + { + "result": "Added parser (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 4 agents.", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": null, + "delivered": {} + } + ] + }, + { + "about": "A member works; a member that leads becomes a manager when it adds one; stopping all its members makes the lead a worker again (min 1).", + "start": { + "min": 1, + "max": 6, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "api", + "role": "owns a part", + "task": "Do the part.", + "lead": true, + "goal": "Ship v2" + } + }, + { + "op": "call", + "by": "api", + "tool": "edit", + "args": {} + }, + { + "op": "call", + "by": "api", + "tool": "team_add", + "args": { + "name": "routes", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "api", + "tool": "edit", + "args": {} + }, + { + "op": "call", + "by": "routes", + "tool": "edit", + "args": {} + }, + { + "op": "call", + "by": "lead", + "tool": "team_stop", + "args": { + "name": "all" + } + }, + { + "op": "call", + "by": "lead", + "tool": "edit", + "args": {} + } + ], + "expect": [ + { + "result": "Added api (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 6 agents.", + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 6 agents.", + "delivered": {} + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + }, + { + "result": null, + "delivered": {} + }, + { + "result": "Stopped api, routes. The team is 1 of 6 agents.", + "delivered": {} + }, + { + "result": null, + "delivered": {} + } + ] + }, + { + "about": "A finished member still makes its manager a manager: it can be woken.", + "start": { + "min": 1, + "max": 4, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "finish", + "by": "parser", + "reply": "Done." + }, + { + "op": "call", + "by": "lead", + "tool": "write", + "args": {} + } + ], + "expect": [ + { + "result": "Added parser (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 4 agents.", + "delivered": {} + }, + { + "delivered": { + "lead": [ + "[Team report from parser to lead:]\nDone." + ] + } + }, + { + "result": "You lead a team: give this to a member (team_add or team_send).", + "delivered": {} + } + ] + }, + { + "about": "A message to one name reaches that one only, under the message mark; the answer names who it went to.", + "start": { + "min": 1, + "max": 4, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "Ship the parser" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "lexer", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "parser", + "tool": "team_send", + "args": { + "to": [ + "lexer" + ], + "message": "I need a token for `=>`." + } + }, + { + "op": "call", + "by": "lexer", + "tool": "team_send", + "args": { + "to": [ + "parser", + "lead" + ], + "message": "Added; see src/lex.ts:40." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_send", + "args": { + "to": [ + "lexer", + "lexer" + ], + "message": "Thanks." + } + } + ], + "expect": [ + { + "result": "Added parser (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 4 agents.", + "delivered": {} + }, + { + "result": "Added lexer (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 4 agents.", + "delivered": {} + }, + { + "result": "Sent to lexer.", + "delivered": { + "lexer": [ + "[Team message from parser to lexer:]\nI need a token for `=>`." + ] + } + }, + { + "result": "Sent to parser, lead.", + "delivered": { + "parser": [ + "[Team message from lexer to parser, lead:]\nAdded; see src/lex.ts:40." + ], + "lead": [ + "[Team message from lexer to parser, lead:]\nAdded; see src/lex.ts:40." + ] + } + }, + { + "result": "Sent to lexer.", + "delivered": { + "lexer": [ + "[Team message from lead to lexer:]\nThanks." + ] + } + } + ] + }, + { + "about": "all reaches everyone but the sender, across the tree; a message is addressed the same way to every recipient.", + "start": { + "min": 1, + "max": 6, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "api", + "role": "owns a part", + "task": "Do the part.", + "lead": true, + "goal": "Ship v2" + } + }, + { + "op": "call", + "by": "api", + "tool": "team_add", + "args": { + "name": "routes", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "web", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "routes", + "tool": "team_send", + "args": { + "to": [ + "all" + ], + "message": "The API is frozen from now on." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_send", + "args": { + "to": "all", + "message": "Stand-up: post your status." + } + } + ], + "expect": [ + { + "result": "Added api (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 6 agents.", + "delivered": {} + }, + { + "result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 6 agents.", + "delivered": {} + }, + { + "result": "Added web (owns a part): it is working. Its report comes to you when it finishes. The team is 4 of 6 agents.", + "delivered": {} + }, + { + "result": "Sent to all.", + "delivered": { + "lead": [ + "[Team message from routes to all:]\nThe API is frozen from now on." + ], + "api": [ + "[Team message from routes to all:]\nThe API is frozen from now on." + ], + "web": [ + "[Team message from routes to all:]\nThe API is frozen from now on." + ] + } + }, + { + "result": "Sent to all.", + "delivered": { + "api": [ + "[Team message from lead to all:]\nStand-up: post your status." + ], + "routes": [ + "[Team message from lead to all:]\nStand-up: post your status." + ], + "web": [ + "[Team message from lead to all:]\nStand-up: post your status." + ] + } + } + ] + }, + { + "about": "Refused sends post nothing: an unknown name (the first), yourself, all with nobody else.", + "start": { + "min": 1, + "max": 4, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_send", + "args": { + "to": [ + "all" + ], + "message": "Anyone?" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_send", + "args": { + "to": [ + "parser", + "ghost", + "spook" + ], + "message": "x" + } + }, + { + "op": "call", + "by": "parser", + "tool": "team_send", + "args": { + "to": [ + "parser" + ], + "message": "x" + } + }, + { + "op": "call", + "by": "parser", + "tool": "team_send", + "args": { + "to": [ + "lead", + "parser" + ], + "message": "x" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_read", + "args": {} + } + ], + "expect": [ + { + "result": "Nobody else is on the team yet: add a member with team_add.", + "delivered": {} + }, + { + "result": "Added parser (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 4 agents.", + "delivered": {} + }, + { + "result": "No agent called ghost in this team. The team: lead, parser.", + "delivered": {} + }, + { + "result": "That is you: send it to someone else.", + "delivered": {} + }, + { + "result": "That is you: send it to someone else.", + "delivered": {} + }, + { + "room": [], + "delivered": {} + } + ] + }, + { + "about": "A manager is not sent its members' messages to each other; it reads them with team_read. A member sees what it sent and received; a sub-manager what touches its subtree; the lead all.", + "start": { + "min": 1, + "max": 8, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "api", + "role": "owns a part", + "task": "Do the part.", + "lead": true, + "goal": "Ship v2" + } + }, + { + "op": "call", + "by": "api", + "tool": "team_add", + "args": { + "name": "routes", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "api", + "tool": "team_add", + "args": { + "name": "db", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "web", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "routes", + "tool": "team_send", + "args": { + "to": [ + "db" + ], + "message": "Which table?" + } + }, + { + "op": "call", + "by": "db", + "tool": "team_send", + "args": { + "to": [ + "routes" + ], + "message": "users." + } + }, + { + "op": "call", + "by": "web", + "tool": "team_send", + "args": { + "to": [ + "lead" + ], + "message": "Mock data ok?" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_send", + "args": { + "to": [ + "web" + ], + "message": "Yes." + } + }, + { + "op": "call", + "by": "web", + "tool": "team_send", + "args": { + "to": [ + "routes" + ], + "message": "Path for login?" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_read", + "args": {} + }, + { + "op": "call", + "by": "api", + "tool": "team_read", + "args": {} + }, + { + "op": "call", + "by": "db", + "tool": "team_read", + "args": {} + }, + { + "op": "call", + "by": "web", + "tool": "team_read", + "args": { + "since": 3 + } + } + ], + "expect": [ + { + "result": "Added api (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 8 agents.", + "delivered": {} + }, + { + "result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 8 agents.", + "delivered": {} + }, + { + "result": "Added db (owns a part): it is working. Its report comes to you when it finishes. The team is 4 of 8 agents.", + "delivered": {} + }, + { + "result": "Added web (owns a part): it is working. Its report comes to you when it finishes. The team is 5 of 8 agents.", + "delivered": {} + }, + { + "result": "Sent to db.", + "delivered": { + "db": [ + "[Team message from routes to db:]\nWhich table?" + ] + } + }, + { + "result": "Sent to routes.", + "delivered": { + "routes": [ + "[Team message from db to routes:]\nusers." + ] + } + }, + { + "result": "Sent to lead.", + "delivered": { + "lead": [ + "[Team message from web to lead:]\nMock data ok?" + ] + } + }, + { + "result": "Sent to web.", + "delivered": { + "web": [ + "[Team message from lead to web:]\nYes." + ] + } + }, + { + "result": "Sent to routes.", + "delivered": { + "routes": [ + "[Team message from web to routes:]\nPath for login?" + ] + } + }, + { + "room": [ + { + "n": 1, + "from": "routes", + "to": "db" + }, + { + "n": 2, + "from": "db", + "to": "routes" + }, + { + "n": 3, + "from": "web", + "to": "lead" + }, + { + "n": 4, + "from": "lead", + "to": "web" + }, + { + "n": 5, + "from": "web", + "to": "routes" + } + ], + "delivered": {} + }, + { + "room": [ + { + "n": 1, + "from": "routes", + "to": "db" + }, + { + "n": 2, + "from": "db", + "to": "routes" + }, + { + "n": 5, + "from": "web", + "to": "routes" + } + ], + "delivered": {} + }, + { + "room": [ + { + "n": 1, + "from": "routes", + "to": "db" + }, + { + "n": 2, + "from": "db", + "to": "routes" + } + ], + "delivered": {} + }, + { + "room": [ + { + "n": 4, + "from": "lead", + "to": "web" + }, + { + "n": 5, + "from": "web", + "to": "routes" + } + ], + "delivered": {} + } + ] + }, + { + "about": "A member's final reply is its report to its manager, under the report mark, and in the room; an empty one says so; a turn that ended at its limit says how.", + "start": { + "min": 1, + "max": 4, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "Ship the parser" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "lexer", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "finish", + "by": "lead" + }, + { + "op": "finish", + "by": "parser", + "reply": "The parser handles `=>`; tests in tests/parse.test.ts pass." + }, + { + "op": "state" + }, + { + "op": "finish", + "by": "lexer", + "reply": "", + "reason": "steps" + }, + { + "op": "call", + "by": "lead", + "tool": "team_read", + "args": { + "since": 0 + } + } + ], + "expect": [ + { + "result": "Added parser (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 4 agents.", + "delivered": {} + }, + { + "result": "Added lexer (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 4 agents.", + "delivered": {} + }, + { + "delivered": {} + }, + { + "delivered": { + "lead": [ + "[Team report from parser to lead:]\nThe parser handles `=>`; tests in tests/parse.test.ts pass." + ] + } + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "parser", + "manager": "lead", + "state": "finished" + }, + { + "name": "lexer", + "manager": "lead", + "state": "working" + } + ], + { + "delivered": { + "lead": [ + "[Team report from lexer to lead:]\n(no report)\n\n[ended: steps]" + ] + } + }, + { + "room": [ + { + "n": 1, + "from": "parser", + "to": "lead" + }, + { + "n": 2, + "from": "lexer", + "to": "lead" + } + ], + "delivered": {} + } + ] + }, + { + "about": "A sub-manager's members report to it, not to the lead; its own final reply is its report to the lead.", + "start": { + "min": 1, + "max": 5, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "api", + "role": "owns a part", + "task": "Do the part.", + "lead": true, + "goal": "Ship v2" + } + }, + { + "op": "call", + "by": "api", + "tool": "team_add", + "args": { + "name": "routes", + "role": "owns a part", + "task": "Do the part.", + "goal": "The REST API" + } + }, + { + "op": "finish", + "by": "api", + "reply": "" + }, + { + "op": "state" + }, + { + "op": "finish", + "by": "routes", + "reply": "Routes done." + }, + { + "op": "state" + }, + { + "op": "finish", + "by": "api", + "reply": "API done: routes by routes." + } + ], + "expect": [ + { + "result": "Added api (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 5 agents.", + "delivered": {} + }, + { + "result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 5 agents.", + "delivered": {} + }, + { + "delivered": { + "lead": [ + "[Team report from api to lead:]\n(no report)" + ] + } + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "api", + "manager": "lead", + "state": "finished" + }, + { + "name": "routes", + "manager": "api", + "state": "working" + } + ], + { + "delivered": { + "api": [ + "[Team report from routes to api:]\nRoutes done." + ] + } + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "api", + "manager": "lead", + "state": "working" + }, + { + "name": "routes", + "manager": "api", + "state": "finished" + } + ], + { + "delivered": { + "lead": [ + "[Team report from api to lead:]\nAPI done: routes by routes." + ] + } + } + ] + }, + { + "about": "A message wakes a finished member with its context; its next final reply is its next report.", + "start": { + "min": 1, + "max": 4, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "finish", + "by": "parser", + "reply": "Done." + }, + { + "op": "state" + }, + { + "op": "call", + "by": "lead", + "tool": "team_send", + "args": { + "to": [ + "parser" + ], + "message": "Also handle `<=`." + } + }, + { + "op": "state" + }, + { + "op": "finish", + "by": "parser", + "reply": "Both done." + } + ], + "expect": [ + { + "result": "Added parser (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 4 agents.", + "delivered": {} + }, + { + "delivered": { + "lead": [ + "[Team report from parser to lead:]\nDone." + ] + } + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "parser", + "manager": "lead", + "state": "finished" + } + ], + { + "result": "Sent to parser.", + "delivered": { + "parser": [ + "[Team message from lead to parser:]\nAlso handle `<=`." + ] + } + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "parser", + "manager": "lead", + "state": "working" + } + ], + { + "delivered": { + "lead": [ + "[Team report from parser to lead:]\nBoth done." + ] + } + } + ] + }, + { + "about": "A report wakes the idle lead.", + "start": { + "min": 1, + "max": 4, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "parser", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "finish", + "by": "lead" + }, + { + "op": "state" + }, + { + "op": "finish", + "by": "parser", + "reply": "Done." + }, + { + "op": "state" + } + ], + "expect": [ + { + "result": "Added parser (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 4 agents.", + "delivered": {} + }, + { + "delivered": {} + }, + [ + { + "name": "lead", + "manager": null, + "state": "idle" + }, + { + "name": "parser", + "manager": "lead", + "state": "working" + } + ], + { + "delivered": { + "lead": [ + "[Team report from parser to lead:]\nDone." + ] + } + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "parser", + "manager": "lead", + "state": "finished" + } + ] + ] + }, + { + "about": "Stopping a member stops its subtree; they leave the team, and their names are free again. Only members under the caller may be stopped.", + "start": { + "min": 1, + "max": 8, + "capacity": {} + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "api", + "role": "owns a part", + "task": "Do the part.", + "lead": true, + "goal": "Ship v2" + } + }, + { + "op": "call", + "by": "api", + "tool": "team_add", + "args": { + "name": "routes", + "role": "owns a part", + "task": "Do the part.", + "lead": true + } + }, + { + "op": "call", + "by": "routes", + "tool": "team_add", + "args": { + "name": "auth", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "api", + "tool": "team_add", + "args": { + "name": "db", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "web", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "api", + "tool": "team_stop", + "args": { + "name": "web" + } + }, + { + "op": "call", + "by": "api", + "tool": "team_stop", + "args": { + "name": "api" + } + }, + { + "op": "call", + "by": "api", + "tool": "team_stop", + "args": { + "name": "ghost" + } + }, + { + "op": "call", + "by": "api", + "tool": "team_stop", + "args": { + "name": "routes" + } + }, + { + "op": "state" + }, + { + "op": "call", + "by": "api", + "tool": "team_add", + "args": { + "name": "auth", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_stop", + "args": { + "name": "all" + } + }, + { + "op": "state" + }, + { + "op": "call", + "by": "lead", + "tool": "team_stop", + "args": { + "name": "all" + } + } + ], + "expect": [ + { + "result": "Added api (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 8 agents.", + "delivered": {} + }, + { + "result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 8 agents.", + "delivered": {} + }, + { + "result": "Added auth (owns a part): it is working. Its report comes to you when it finishes. The team is 4 of 8 agents.", + "delivered": {} + }, + { + "result": "Added db (owns a part): it is working. Its report comes to you when it finishes. The team is 5 of 8 agents.", + "delivered": {} + }, + { + "result": "Added web (owns a part): it is working. Its report comes to you when it finishes. The team is 6 of 8 agents.", + "delivered": {} + }, + { + "result": "web is not under you: you can stop only the members you lead, directly or through your members.", + "delivered": {} + }, + { + "result": "api is not under you: you can stop only the members you lead, directly or through your members.", + "delivered": {} + }, + { + "result": "No agent called ghost in this team. The team: lead, api, routes, auth, db, web.", + "delivered": {} + }, + { + "result": "Stopped routes, auth. The team is 4 of 8 agents.", + "delivered": {} + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "api", + "manager": "lead", + "state": "working" + }, + { + "name": "db", + "manager": "api", + "state": "working" + }, + { + "name": "web", + "manager": "lead", + "state": "working" + } + ], + { + "result": "Added auth (owns a part): it is working. Its report comes to you when it finishes. The team is 5 of 8 agents.", + "delivered": {} + }, + { + "result": "Stopped api, db, auth, web. The team is 1 of 8 agents.", + "delivered": {} + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + } + ], + { + "result": "No member is under you: there is nobody to stop.", + "delivered": {} + } + ] + }, + { + "about": "A member the person stopped is said to the lead in a note, with those under it.", + "start": { + "min": 1, + "max": 6, + "capacity": {} + }, + "steps": [ + { + "op": "user" + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "api", + "role": "owns a part", + "task": "Do the part.", + "lead": true, + "goal": "Ship v2" + } + }, + { + "op": "call", + "by": "api", + "tool": "team_add", + "args": { + "name": "routes", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "finish", + "by": "lead" + }, + { + "op": "person_stop", + "name": "api" + }, + { + "op": "user" + }, + { + "op": "state" + } + ], + "expect": [ + { + "note": "\nThe team may have 1 to 6 agents, you included.\n" + }, + { + "result": "Added api (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 6 agents.", + "delivered": {} + }, + { + "result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 6 agents.", + "delivered": {} + }, + { + "delivered": {} + }, + null, + { + "note": "\nThe person stopped api, routes; they have left the team.\n" + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + } + ] + ] + }, + { + "about": "Past the model's parallel capacity a member waits for a slot; slots free in order as turns end; the lead goes first.", + "start": { + "min": 1, + "max": 6, + "capacity": { + "parallel": 2 + } + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "a", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "b", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "c", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "state" + }, + { + "op": "finish", + "by": "lead" + }, + { + "op": "state" + }, + { + "op": "finish", + "by": "a", + "reply": "a done" + }, + { + "op": "state" + }, + { + "op": "finish", + "by": "b", + "reply": "b done" + }, + { + "op": "state" + } + ], + "expect": [ + { + "result": "Added a (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 6 agents.", + "delivered": {} + }, + { + "result": "Added b (owns a part): it waits for a free slot — the model serves 2 requests at once and all are busy — and starts when one frees. Its report comes to you when it finishes. The team is 3 of 6 agents.", + "delivered": {} + }, + { + "result": "Added c (owns a part): it waits for a free slot — the model serves 2 requests at once and all are busy — and starts when one frees. Its report comes to you when it finishes. The team is 4 of 6 agents.", + "delivered": {} + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "a", + "manager": "lead", + "state": "working" + }, + { + "name": "b", + "manager": "lead", + "state": "waiting" + }, + { + "name": "c", + "manager": "lead", + "state": "waiting" + } + ], + { + "delivered": {} + }, + [ + { + "name": "lead", + "manager": null, + "state": "idle" + }, + { + "name": "a", + "manager": "lead", + "state": "working" + }, + { + "name": "b", + "manager": "lead", + "state": "working" + }, + { + "name": "c", + "manager": "lead", + "state": "waiting" + } + ], + { + "delivered": { + "lead": [ + "[Team report from a to lead:]\na done" + ] + } + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "a", + "manager": "lead", + "state": "finished" + }, + { + "name": "b", + "manager": "lead", + "state": "working" + }, + { + "name": "c", + "manager": "lead", + "state": "waiting" + } + ], + { + "delivered": { + "lead": [ + "[Team report from b to lead:]\nb done" + ] + } + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "a", + "manager": "lead", + "state": "finished" + }, + { + "name": "b", + "manager": "lead", + "state": "finished" + }, + { + "name": "c", + "manager": "lead", + "state": "working" + } + ] + ] + }, + { + "about": "A stopped member's slot goes to the next waiting one; a waiting member that is stopped leaves the queue.", + "start": { + "min": 1, + "max": 6, + "capacity": { + "parallel": 2 + } + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "a", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "b", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "c", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_stop", + "args": { + "name": "b" + } + }, + { + "op": "state" + }, + { + "op": "call", + "by": "lead", + "tool": "team_stop", + "args": { + "name": "a" + } + }, + { + "op": "state" + } + ], + "expect": [ + { + "result": "Added a (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 6 agents.", + "delivered": {} + }, + { + "result": "Added b (owns a part): it waits for a free slot — the model serves 2 requests at once and all are busy — and starts when one frees. Its report comes to you when it finishes. The team is 3 of 6 agents.", + "delivered": {} + }, + { + "result": "Added c (owns a part): it waits for a free slot — the model serves 2 requests at once and all are busy — and starts when one frees. Its report comes to you when it finishes. The team is 4 of 6 agents.", + "delivered": {} + }, + { + "result": "Stopped b. The team is 3 of 6 agents.", + "delivered": {} + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "a", + "manager": "lead", + "state": "working" + }, + { + "name": "c", + "manager": "lead", + "state": "waiting" + } + ], + { + "result": "Stopped a. The team is 2 of 6 agents.", + "delivered": {} + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "c", + "manager": "lead", + "state": "working" + } + ] + ] + }, + { + "about": "A message to a finished member while the model is full: it waits for a slot.", + "start": { + "min": 1, + "max": 6, + "capacity": { + "parallel": 2 + } + }, + "steps": [ + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "a", + "role": "owns a part", + "task": "Do the part.", + "goal": "G" + } + }, + { + "op": "finish", + "by": "a", + "reply": "done" + }, + { + "op": "call", + "by": "lead", + "tool": "team_add", + "args": { + "name": "b", + "role": "owns a part", + "task": "Do the part." + } + }, + { + "op": "call", + "by": "lead", + "tool": "team_send", + "args": { + "to": [ + "a" + ], + "message": "One more thing." + } + }, + { + "op": "state" + }, + { + "op": "finish", + "by": "b", + "reply": "b done" + }, + { + "op": "state" + } + ], + "expect": [ + { + "result": "Added a (owns a part): it is working. Its report comes to you when it finishes. The team is 2 of 6 agents.", + "delivered": {} + }, + { + "delivered": { + "lead": [ + "[Team report from a to lead:]\ndone" + ] + } + }, + { + "result": "Added b (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 6 agents.", + "delivered": {} + }, + { + "result": "Sent to a.", + "delivered": { + "a": [ + "[Team message from lead to a:]\nOne more thing." + ] + } + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "a", + "manager": "lead", + "state": "waiting" + }, + { + "name": "b", + "manager": "lead", + "state": "working" + } + ], + { + "delivered": { + "lead": [ + "[Team report from b to lead:]\nb done" + ] + } + }, + [ + { + "name": "lead", + "manager": null, + "state": "working" + }, + { + "name": "a", + "manager": "lead", + "state": "working" + }, + { + "name": "b", + "manager": "lead", + "state": "finished" + } + ] + ] + } + ] +} diff --git a/harness/loop.json b/harness/loop.json index 3df9140..a39721f 100644 --- a/harness/loop.json +++ b/harness/loop.json @@ -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": "", "close": "", "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 diff --git a/harness/permission/defaults.json b/harness/permission/defaults.json index 8e8acf4..535f27b 100644 --- a/harness/permission/defaults.json +++ b/harness/permission/defaults.json @@ -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", diff --git a/harness/prompts/blocks/team_lead.md b/harness/prompts/blocks/team_lead.md new file mode 100644 index 0000000..ea4980a --- /dev/null +++ b/harness/prompts/blocks/team_lead.md @@ -0,0 +1,12 @@ + +# 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. diff --git a/harness/prompts/blocks/team_member.md b/harness/prompts/blocks/team_member.md new file mode 100644 index 0000000..3032b49 --- /dev/null +++ b/harness/prompts/blocks/team_member.md @@ -0,0 +1,14 @@ + +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}} diff --git a/harness/prompts/index.json b/harness/prompts/index.json index a99d073..9f330d3 100644 --- a/harness/prompts/index.json +++ b/harness/prompts/index.json @@ -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/.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" + } } } } diff --git a/harness/team.md b/harness/team.md new file mode 100644 index 0000000..7a4f5d8 --- /dev/null +++ b/harness/team.md @@ -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 — `), + 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 `: `), 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. diff --git a/harness/tools/team_add.json b/harness/tools/team_add.json new file mode 100644 index 0000000..c880d10 --- /dev/null +++ b/harness/tools/team_add.json @@ -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)" + } +} diff --git a/harness/tools/team_read.json b/harness/tools/team_read.json new file mode 100644 index 0000000..c59c116 --- /dev/null +++ b/harness/tools/team_read.json @@ -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)" + } + } + } +} diff --git a/harness/tools/team_send.json b/harness/tools/team_send.json new file mode 100644 index 0000000..88da764 --- /dev/null +++ b/harness/tools/team_send.json @@ -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" + ] + } +} diff --git a/harness/tools/team_stop.json b/harness/tools/team_stop.json new file mode 100644 index 0000000..94b5d94 --- /dev/null +++ b/harness/tools/team_stop.json @@ -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" + ] + } +} diff --git a/src/harness.ts b/src/harness.ts index 659c7f1..7234f71 100644 --- a/src/harness.ts +++ b/src/harness.ts @@ -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, narrow: Na } export const TOOL_SPECS: Record = 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 diff --git a/src/prompt/assemble.ts b/src/prompt/assemble.ts index 347845f..3067b08 100644 --- a/src/prompt/assemble.ts +++ b/src/prompt/assemble.ts @@ -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 = { "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, diff --git a/tests/harness.test.ts b/tests/harness.test.ts index 28aaef9..769c0de 100644 --- a/tests/harness.test.ts +++ b/tests/harness.test.ts @@ -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 } for (const n of ["concise", "pragmatic", "optimistic", "funny", "formal", "socratic"]) {