diff --git a/harness/README.md b/harness/README.md index a36884d..4eb5677 100644 --- a/harness/README.md +++ b/harness/README.md @@ -15,18 +15,19 @@ page *Harness-parity* in LLeMbas CLI's wiki. |---|---| | `VERSION` | The spec's own version, apart from LLeMbas CLI's | | `purpose.json` | The `purpose` argument added to a tool whose calls say what they are for | -| `tools/.json` | One tool each: name, `scope`, `sessions`, `risk`, `exclusive`, `block` (how a call is shown: `shell`, `edit`, `read`, `search`, `web`, `task`, `plan` — not drawn, the plan card refreshes — `hidden`, `tool`), `purpose`, former names (`aliases`, per project), renamed arguments, `variables`, what a project offers of the parameters (`narrow`, per project), `offered` (`subagent`: given to subagents only, never to the main agent — `report`), `requires` and `unavailable` (when it can be offered, and what a call answers when that has gone), `confirm` (`always`: the person is asked in every mode), the description and the JSON Schema of its parameters — exactly what a model is sent | +| `tools/.json` | One tool each: name, `scope`, `sessions`, `risk`, `exclusive`, `block` (how a call is shown: `shell`, `edit`, `read`, `search`, `web`, `task`, `plan` — not drawn, the plan card refreshes — `hidden`, `tool`), `purpose`, former names (`aliases`, per project), renamed arguments, `variables`, what a project offers of the parameters (`narrow`, per project), `offered` (`subagent`: given to subagents only, never to the main agent — `report`, `team_finish`, `team_request`; `main`, spec 3.4.0: given to the main agent only, never to a subagent — `team_add`, `team_assign`, `team_replace`, `team_stop`, `team_decide`), `requires` and `unavailable` (when it can be offered, and what a call answers when that has gone), `confirm` (`always`: the person is asked in every mode), the description and the JSON Schema of its parameters — exactly what a model is sent | | `prompts/` | The prompt texts; `prompts/index.json` says which are shared, which LLeMbas fragment each one is, and what each `{{variable}}` in a text means. `prompts/personality/` holds the personality presets both offer | | `permission/hardline.json` | The floor: commands refused in every mode, protected paths, and how a command is spelled plainly before it is checked again | | `permission/arity.json` | How much of a command an "always allow" covers | | `permission/defaults.json` | The rules every session starts from | | `permission/modes.json` | The four modes — manual, edit, auto, plan — as what each does with each risk class | | `models/schema.json` | Model metadata: context, output limit, efforts and the thinking switch, vision, tools, notes, capacity (`parallel`); `levels` — the effort levels a chat offers and what each sends | -| `loop.json` | How one prompt runs: the step backstop, budgets, wrap-up, nudges, parallel calls, capacity; `jobs` — background work, its marks and limits; `subagents` — the model refusals, what names a model, the setting's label; `team` — a team's size, names, marks, refusals and texts; `context` — result ids, stubs, the output cap, the compaction table, the environment lines, the session notes | +| `loop.json` | How one prompt runs: the step backstop, budgets, wrap-up, nudges, parallel calls, capacity; `jobs` — background work, its marks and limits; `subagents` — the model refusals, what names a model, the setting's label; `team` — a team's size, names, marks, refusals and texts; `progress` — the phases' texts; `context` — result ids, stubs, the output cap, the compaction table, the environment lines, the session notes | | `filesystem.md` | The file system a session works in: paths, node kinds, the four access levels, errors, listings, search and indexing by tokens, stacked instruction files, skills, memory | | `context.md` | Context hygiene: what happens to a tool result when it is inserted, and never afterwards; the prefix that stays still; reasoning sent back during its turn; no absolute paths in the prompt | | `team.md` | Teams: a lead and the members it adds, the size setting, the room, who receives what, reports, a manager's refusals, capacity, stopping | -| `subagents.md` | Subagents: the word, which model one runs on (the session's, or one the person named), the capacity refusals, and when the Subagents setting is offered | +| `subagents.md` | Subagents: the word, which model one runs on (the session's, or one the person named), the capacity refusals, and when the Subagents setting is offered; from 3.4.0 (§5–§13) the main agent and its tree — only it adds, permissions inherited and never widened, request options, a subagent's life and its findings, what clients show | +| `progress.md` | Progress (3.4.0): the thirteen phases every client shows the same way, the data each carries, their texts, and where they are sent | | `quirks.json` | What real servers do, and how both harnesses cope | | `conformance/.json` | Cases with their expected results, run by both test suites | | `acp.md` | The link between them: ACP plus every `_lembas/*` method, field and capability | @@ -70,6 +71,37 @@ page *Harness-parity* in LLeMbas CLI's wiki. approval in plan mode and answers its `unavailable` text outside it. Their permission key is `plan` (`permission/defaults.json`), which a rule written for the former `todo` key still reaches. +## 3.4.0 + +Written ahead of both implementations; the server vendors it as written, both carry it in the same +round (`progress.md`, `subagents.md` §5–§13). + +- **Progress** (`progress.md`, `loop.json` `progress`): one fixed set of phases — `queued`, + `waiting_for_model`, `loading_model`, `reading_prompt`, `thinking`, `writing`, `running_tool`, + `waiting_for_person`, `waiting_for_subagents`, `compacting`, `retrying`, `stopped`, `failed` — + each a structured object `{ phase, since, …data }` with fixed fields, an English text that is + the shared i18n key, and one line every client draws: `text · elapsed`. Sent beside the old + `status` text in `/api/v2` and over the link (`acp.md` capability `progress`: the `phase` event, + `progress` in `status`); `status` stays. Area `progress`. +- **Subagents v2** (`subagents.md` §5–§13; supersedes `team.md` where they differ, which says so): + the **main agent** is the session's, permanent, and **alone adds, assigns, replaces, stops and + decides** (`offered: main`); a member with `lead: true` is a **sub-manager** that directs those + placed under it (`team_add`'s `parent`). Every subagent has an id (`s1`…), a name, a position, a + parent, a task, a mode and a model. Min above 1 opens `min − 1` **slots** from the start of the + reply. **Permissions are inherited and never widened**: a subagent's mode is its parent's or + stricter; a narrowed one may get **request options** (`read`, `write`, `execute`) and ask the + main agent (`team_request` → `team_decide`, a grant that never reaches past the main agent's own + mode); a call nobody in the tree can allow goes **to the person**. A subagent ends with + `team_finish`, is stopped, or **runs out of context**: paused, one final turn to save its + findings and tell its parent, then `out_of_context` and a report; findings and transcripts are + kept, readable by the parent, the main agent and the person. New tools `team_whoami`, + `team_finish`, `team_findings`, `team_request`, `team_decide`, `team_assign`, `team_replace`; + `team_add` gains `parent`, `mode`, `request`; `team_send` takes `parent`; `team_stop` is the main + agent's. `blocks/team_submanager.md`; `team_lead.md` and `team_member.md` reworded. Over the link, + capability `tree`: the `team` event, `_lembas/team`, `/transcript` (read-only), `/findings`, + `/stop`, `subagent` on an approval. Areas `team` (cases changed where a member used to add or + stop, and new ones) and `delegation`. + ## 3.3.0 Written ahead of both implementations as LLeMbas CLI 02c7282; both carry it in the same round (`subagents.md`). diff --git a/harness/VERSION b/harness/VERSION index 15a2799..1809198 100644 --- a/harness/VERSION +++ b/harness/VERSION @@ -1 +1 @@ -3.3.0 +3.4.0 diff --git a/harness/acp.md b/harness/acp.md index 52da28d..a1bd0f9 100644 --- a/harness/acp.md +++ b/harness/acp.md @@ -65,6 +65,8 @@ change to the protocol is a change here first, then in both projects. | `rewind` | `_lembas/session/rewind` | | `show` | spec 3.0.0: `_lembas/show`; the `show` tool is offered in a session the client opened when the client said `show: true` in `clientCapabilities._meta.lembas` | | `jobs` | background work (spec 2.0.0): the `job` and `job_note` events; `_lembas/jobs`, `_lembas/job/output`, `_lembas/job/stop`, `_lembas/job/send`, `_lembas/job/transcript`; `jobs` in `_lembas/session/status`; `job` on `session/request_permission`; `machine` on a `prompt` event | +| `progress` | spec 3.4.0 (`progress.md`): the `phase` event; `progress` in `_lembas/session/status` | +| `tree` | spec 3.4.0 (`subagents.md` §5–§13): the `team` event; `_lembas/team`, `_lembas/team/transcript`, `_lembas/team/findings`, `_lembas/team/stop`; `subagent` on `session/request_permission` | ## Standard ACP, as used here @@ -245,7 +247,9 @@ with options `once` (allow_once), `session` (allow_always, absent where "always" and `deny` (reject_once), and `_meta.lembas: { reason, purpose?, always?, patient?, job? }`. `patient`: a person may also answer at the terminal (a shared session), so the client does not time it out. `job` (capability `jobs`): `{ id, agent, description }` when a subagent running as a background job asks, so the card -can say which — a background subagent may ask while the session itself is idle. +can say which — a background subagent may ask while the session itself is idle. `subagent` (capability `tree`, spec 3.4.0): +`{ id, name, position }` when the call is a tree subagent's that nobody in the tree could allow (`subagents.md` §7) — +the card names it; the answers are the same as for the main agent's own call. Answer: ACP's `{ outcome: { outcome: "selected", optionId } | { outcome: "cancelled" } }`, with optional `_meta.lembas.command` (the corrected command line, `once` only) or `_meta.lembas.feedback` (why it was denied). No answer within `remote.approval_timeout` is a denial. @@ -275,7 +279,7 @@ client hears the new limits. `{ sessionId, event }`: every engine event of the session, to a client that said `_meta.lembas`. `event.type` is one of `session`, `prompt`, `step`, `title`, `text`, `reasoning`, `tool_call_delta`, `progress`, `tool_start`, `tool_end`, `tool_denied`, `usage`, `inbox`, -`steered`, `task`, `notice`, `retract`, `plan`, `compacted`, `sub_tool`, `job`, `job_note`, `mode`, `model`, `flash`, +`steered`, `task`, `notice`, `retract`, `plan`, `compacted`, `sub_tool`, `job`, `job_note`, `phase`, `team`, `mode`, `model`, `flash`, `setting`, `error`, `done` (`src/bus/index.ts`). Those this protocol relies on: | Event | Fields | @@ -291,6 +295,8 @@ client hears the new limits. | `job` | `state: "start" \| "update" \| "end"`, `job: JobInfo` (capability `jobs`) — a background job of the session began, has news (new output, a subagent's step; at most four a second per job), or ended. `JobInfo = { id, kind: "command" \| "subagent", state: "running" \| "done" \| "stopped" \| "error" \| "lost", description, command?, agent?, model?, started, ended?, exit?, unread, steps?, tools?, ownerJobId? }` — `description` what is shown for it (a command's `description` or its first line; a subagent's description), `unread` characters of output the model has not read, `exit` a command's exit code (null: a signal), `ownerJobId` set on a job a subagent started, `lost` a job that was running when the device's process ended | | `job_note` | `jobId`, `text` (capability `jobs`) — the note about that job was given to the model: inside a turn at the next step, or as the `machine` turn whose `prompt` follows | | `sub_tool` | `callId` (the `task` call the subagent belongs to), `jobId?` (its job, when it runs as one), `name`, `title`, `isError?` — a tool call of a subagent finished; a client draws it under the task line, or in the job's panel | +| `phase` | `progress` — the progress object (`progress.md` §1), or `null` when nothing runs; `agent?` — the subagent's id when it is a subagent's, absent for the main agent's (capability `progress`). Sent when the phase changes, and for a change of its data at most four times a second. The existing `progress` event (the prompt's tokens read) is unchanged | +| `team` | `tree` — the session's subagent tree (`subagents.md` §12, the data) — whenever it changes, at most four times a second (capability `tree`). A client draws it and fetches nothing else to do so | ### `_lembas/directories` — client → agent → result @@ -319,9 +325,9 @@ with the message in the store; `attachments`, and the text without them) or `{ r ### `_lembas/session/status` — client → agent → result Params `{ sessionId }`. Result -`{ sessionId, exists, held: "here" | "terminal" | "none", busy, title, turnId?, queued: string[], lastTurnId?, plan?, jobs? }` +`{ sessionId, exists, held: "here" | "terminal" | "none", busy, title, turnId?, queued: string[], lastTurnId?, plan?, jobs?, progress?, tree? }` — `plan` the session's plan or `null` (capability `plan_state`), so a client reattaching draws it -without waiting for the next change; `jobs` the session's background jobs, `JobInfo[]` running first (capability `jobs`), for the same reason; the turn fields with capability `turns`: the turn running now, the ones waiting, the last +without waiting for the next change; `progress` the main agent's progress object or `null` (capability `progress`), and `tree` the subagent tree or `null` when the session has none (capability `tree`), for the same reason; `jobs` the session's background jobs, `JobInfo[]` running first (capability `jobs`), for the same reason; the turn fields with capability `turns`: the turn running now, the ones waiting, the last one finished (for a session not open, `held: "none"`, the last one it finished before). **Reattaching** after a restart or a dropped link, for an incomplete row with turn T: T running → follow it; T queued → wait, and follow it when it starts; T done → import it from `history`; T unknown → send the prompt again (safe: the device deduplicates). @@ -362,6 +368,30 @@ with its output — so a client can show the subagent as a panel and follow it w `job` events that come after. `-32602` for a command job. A subagent's transcript lasts as long as its session row on the device: it is kept with the parent session and deleted with it. +### `_lembas/team` — client → agent → result (capability `tree`) + +Params `{ sessionId, since?: number }`. Result `{ tree, room }`: the subagent tree (`subagents.md` +§12) and the room's messages numbered above `since` (default 0), every one — the person sees the +whole room — each `{ n, from, to, recipients, text, time, report, kind }` with `kind` one of +`message`, `report`, `assignment`, `request`, `decision`. `tree` is `null` for a session with no +subagents. For a session held by a terminal, answered through the hub, as `status` is. + +### `_lembas/team/transcript` — client → agent → result (capability `tree`) + +Params `{ sessionId, id }` — a subagent's id (`s1`), live or ended. Result `{ id, state, turns }` in +the shape of `_lembas/session/history`'s `turns`: its conversation so far, **read-only** — the link +has no method to write into it. A client following it live refetches on the `team` event and draws +the `sub_tool` events meanwhile. `-32602` for an id the session never had. + +### `_lembas/team/findings` — client → agent → result (capability `tree`) + +Params `{ sessionId, id }`. Result `{ id, findings: [{ n, text, time }] }`, ended subagents' too. + +### `_lembas/team/stop` — client → agent → result (capability `tree`) + +Params `{ sessionId, id }`: the person stops a subagent and its subtree (`subagents.md` §10), said to +the main agent as a note. Result `{ stopped: [id, …] }`; `-32602` for an id that is not live. + ### `_lembas/session/delete` — client → agent → result Params `{ sessionId }`: the chat was deleted. The session stops if it works, leaves the store (its diff --git a/harness/conformance/delegation.json b/harness/conformance/delegation.json new file mode 100644 index 0000000..cb42a25 --- /dev/null +++ b/harness/conformance/delegation.json @@ -0,0 +1,431 @@ +{ + "about": "Delegation (subagents.md §7, spec 3.4.0): a subagent's mode and request options, and who decides its calls. The project root is /project, nothing named exists on disk, and only the default rules apply (permission/defaults.json). `add` — team_add's mode check for a parent in `parent_mode` with `mode` and `request` as given → `{result, mode}`: the refusal word for word, or null and the mode the subagent gets. `call` — a subagent in `mode` with `request` options and `grants` (rules {tool, pattern} the main agent allowed) under a main agent in `main_mode`, calling `call` (`bash` with a `line`, or a file tool with a `path` relative to the root) → `{answer, text?}`: `run`, `person` (an approval card to the person), or `refused` with its text where the case gives one (a hardline's reason is each project's). `unattended`: nobody can answer. `request` — team_request for `tool` by a subagent with `request` options, its first → `{result}`. `switch` — the main agent's mode changes from `main_from` to `main_to` with these subagents (each with its parent and mode) → each one's mode after. LLeMbas: services/team.py; CLI: team/delegation.ts.", + "cases": [ + { + "about": "No mode given: the parent's.", + "op": "add", + "parent_mode": "edit", + "expect": { + "result": null, + "mode": "edit" + } + }, + { + "about": "Stricter is allowed.", + "op": "add", + "parent_mode": "edit", + "mode": "manual", + "expect": { + "result": null, + "mode": "manual" + } + }, + { + "about": "Looser is refused, naming the parent's mode.", + "op": "add", + "parent_mode": "manual", + "mode": "edit", + "expect": { + "result": "A subagent's mode cannot be looser than its parent's (manual).", + "mode": null + } + }, + { + "about": "Plan is the strictest: anything else under it is looser.", + "op": "add", + "parent_mode": "plan", + "mode": "auto", + "expect": { + "result": "A subagent's mode cannot be looser than its parent's (plan).", + "mode": null + } + }, + { + "about": "Request options with the parent's own mode: nothing to ask for.", + "op": "add", + "parent_mode": "auto", + "mode": "auto", + "request": [ + "write" + ], + "expect": { + "result": "Request options go with a mode stricter than the parent's: without one there is nothing to ask for.", + "mode": null + } + }, + { + "about": "Request options with no mode given (the parent's): refused the same way.", + "op": "add", + "parent_mode": "edit", + "request": [ + "execute" + ], + "expect": { + "result": "Request options go with a mode stricter than the parent's: without one there is nothing to ask for.", + "mode": null + } + }, + { + "about": "Request options with a stricter mode.", + "op": "add", + "parent_mode": "edit", + "mode": "manual", + "request": [ + "write", + "execute" + ], + "expect": { + "result": null, + "mode": "manual" + } + }, + { + "about": "An unknown kind is refused, naming it.", + "op": "add", + "parent_mode": "edit", + "mode": "manual", + "request": [ + "delete" + ], + "expect": { + "result": "delete is not a request option: use read, write or execute.", + "mode": null + } + }, + { + "about": "An empty list of request options is no options, and needs nothing.", + "op": "add", + "parent_mode": "edit", + "request": [], + "expect": { + "result": null, + "mode": "edit" + } + }, + { + "about": "All in edit: an edit inside the project runs.", + "op": "call", + "main_mode": "edit", + "mode": "edit", + "call": { + "tool": "edit", + "path": "src/a.ts" + }, + "expect": { + "answer": "run" + } + }, + { + "about": "All in edit: bash that edit mode asks goes to the person — the main agent could not allow it either.", + "op": "call", + "main_mode": "edit", + "mode": "edit", + "call": { + "tool": "bash", + "line": "git push origin main" + }, + "expect": { + "answer": "person" + } + }, + { + "about": "All in manual: an edit goes to the person.", + "op": "call", + "main_mode": "manual", + "mode": "manual", + "call": { + "tool": "edit", + "path": "src/a.ts" + }, + "expect": { + "answer": "person" + } + }, + { + "about": "Narrowed with no request options: what the main agent could allow is refused.", + "op": "call", + "main_mode": "edit", + "mode": "manual", + "call": { + "tool": "edit", + "path": "src/a.ts" + }, + "expect": { + "answer": "refused", + "text": "Your mode does not allow this, and you may not ask for it: tell your parent what you need." + } + }, + { + "about": "Narrowed with the kind in its options: it asks the main agent.", + "op": "call", + "main_mode": "edit", + "mode": "manual", + "request": [ + "write" + ], + "call": { + "tool": "edit", + "path": "src/a.ts" + }, + "expect": { + "answer": "refused", + "text": "Your mode does not allow this, but the main agent may: ask for it with team_request (the tool, the pattern, why), then try again once it allows." + } + }, + { + "about": "Narrowed, the kind not in its options.", + "op": "call", + "main_mode": "auto", + "mode": "edit", + "request": [ + "write" + ], + "call": { + "tool": "bash", + "line": "git push origin main" + }, + "expect": { + "answer": "refused", + "text": "Your mode does not allow this, and you may not ask for it: tell your parent what you need." + } + }, + { + "about": "Narrowed, execute in its options: asks the main agent.", + "op": "call", + "main_mode": "auto", + "mode": "edit", + "request": [ + "execute" + ], + "call": { + "tool": "bash", + "line": "git push origin main" + }, + "expect": { + "answer": "refused", + "text": "Your mode does not allow this, but the main agent may: ask for it with team_request (the tool, the pattern, why), then try again once it allows." + } + }, + { + "about": "A grant from the main agent lets the call run.", + "op": "call", + "main_mode": "auto", + "mode": "edit", + "request": [ + "execute" + ], + "grants": [ + { + "tool": "bash", + "pattern": "git push *" + } + ], + "call": { + "tool": "bash", + "line": "git push origin main" + }, + "expect": { + "answer": "run" + } + }, + { + "about": "A grant for another pattern does not.", + "op": "call", + "main_mode": "auto", + "mode": "edit", + "request": [ + "execute" + ], + "grants": [ + { + "tool": "bash", + "pattern": "git push *" + } + ], + "call": { + "tool": "bash", + "line": "git commit -m x" + }, + "expect": { + "answer": "refused", + "text": "Your mode does not allow this, but the main agent may: ask for it with team_request (the tool, the pattern, why), then try again once it allows." + } + }, + { + "about": "A grant never reaches past the main agent's own mode: what it would have to ask goes to the person.", + "op": "call", + "main_mode": "edit", + "mode": "manual", + "request": [ + "execute" + ], + "grants": [ + { + "tool": "bash", + "pattern": "git push *" + } + ], + "call": { + "tool": "bash", + "line": "git push origin main" + }, + "expect": { + "answer": "person" + } + }, + { + "about": "Reading .env asks in every mode: it goes to the person.", + "op": "call", + "main_mode": "edit", + "mode": "edit", + "call": { + "tool": "read", + "path": ".env" + }, + "expect": { + "answer": "person" + } + }, + { + "about": "Reading .env under an auto main agent: a narrowed subagent with read in its options asks it.", + "op": "call", + "main_mode": "auto", + "mode": "manual", + "request": [ + "read" + ], + "call": { + "tool": "read", + "path": ".env" + }, + "expect": { + "answer": "refused", + "text": "Your mode does not allow this, but the main agent may: ask for it with team_request (the tool, the pattern, why), then try again once it allows." + } + }, + { + "about": "Plan mode's refusal is final: request options do not reach past it.", + "op": "call", + "main_mode": "auto", + "mode": "plan", + "request": [ + "execute" + ], + "call": { + "tool": "bash", + "line": "rm -rf build" + }, + "expect": { + "answer": "refused", + "text": "Plan mode: nothing changes but the plan. Put what this would do into the plan with plan_write or plan_update; it can be done once the person approves the plan." + } + }, + { + "about": "What the rules allow runs in every mode.", + "op": "call", + "main_mode": "edit", + "mode": "plan", + "call": { + "tool": "bash", + "line": "git status" + }, + "expect": { + "answer": "run" + } + }, + { + "about": "The hardline refuses in every mode, auto included.", + "op": "call", + "main_mode": "auto", + "mode": "auto", + "call": { + "tool": "bash", + "line": "rm -rf /" + }, + "expect": { + "answer": "refused" + } + }, + { + "about": "Unattended: what would go to the person is refused.", + "op": "call", + "main_mode": "edit", + "mode": "edit", + "unattended": true, + "call": { + "tool": "bash", + "line": "git push origin main" + }, + "expect": { + "answer": "refused" + } + }, + { + "about": "team_request: a tool whose risk class is not in the options is refused, naming them.", + "op": "request", + "request": [ + "write" + ], + "tool": "bash", + "expect": { + "result": "You may not ask for bash: your request options are write." + } + }, + { + "about": "team_request: a kind in the options is asked, with the request's id.", + "op": "request", + "request": [ + "write", + "execute" + ], + "tool": "bash", + "expect": { + "result": "Asked the main agent (r1): you are told its decision. Carry on with what you can meanwhile." + } + }, + { + "about": "The main agent made stricter: every subagent looser than its parent is made as strict as it, down the tree; a stricter one keeps its own.", + "op": "switch", + "agents": [ + { + "name": "api", + "parent": "lead", + "mode": "auto" + }, + { + "name": "tests", + "parent": "api", + "mode": "edit" + }, + { + "name": "docs", + "parent": "lead", + "mode": "plan" + } + ], + "main_from": "auto", + "main_to": "manual", + "expect": { + "api": "manual", + "tests": "manual", + "docs": "plan" + } + }, + { + "about": "The main agent made looser: no subagent changes.", + "op": "switch", + "agents": [ + { + "name": "api", + "parent": "lead", + "mode": "edit" + } + ], + "main_from": "edit", + "main_to": "auto", + "expect": { + "api": "edit" + } + } + ] +} diff --git a/harness/conformance/progress.json b/harness/conformance/progress.json new file mode 100644 index 0000000..6096060 --- /dev/null +++ b/harness/conformance/progress.json @@ -0,0 +1,333 @@ +{ + "about": "Progress (progress.md, spec 3.4.0): the text and the line every client draws for a progress object. `progress`: the object; `now`: the client's clock (Unix ms). Expect `text` — the phase's text from loop.json progress.texts, its fields filled (numbers written plainly: integers as they are, a rate with one decimal) — and `line` — `{{text}} · {{elapsed}}` with elapsed = now − since in whole seconds (5s, 1m 05s, 1h 02m), or the text alone for stopped and failed. retrying's `in` is counted down: in − floor((now − since) / 1000), never below 0. LLeMbas: services/progress.py; CLI: session/progress.ts.", + "cases": [ + { + "about": "Queued, nothing more known.", + "progress": { + "phase": "queued", + "since": 1762790400000 + }, + "now": 1762790405000, + "expect": { + "text": "Queued", + "line": "Queued · 5s" + } + }, + { + "about": "Queued behind two turns.", + "progress": { + "phase": "queued", + "since": 1762790400000, + "ahead": 2 + }, + "now": 1762790465000, + "expect": { + "text": "Queued: 2 ahead", + "line": "Queued: 2 ahead · 1m 05s" + } + }, + { + "about": "A one-request server answering another request.", + "progress": { + "phase": "waiting_for_model", + "since": 1762790400000, + "model": "Qwen 27B", + "reason": "busy" + }, + "now": 1762790412400, + "expect": { + "text": "Waiting for Qwen 27B: it serves one request at a time and is answering another", + "line": "Waiting for Qwen 27B: it serves one request at a time and is answering another · 12s" + } + }, + { + "about": "Every slot taken.", + "progress": { + "phase": "waiting_for_model", + "since": 1762790400000, + "model": "coder", + "reason": "slots" + }, + "now": 1762790401000, + "expect": { + "text": "Waiting for coder: all its slots are in use", + "line": "Waiting for coder: all its slots are in use · 1s" + } + }, + { + "about": "Another model on the one-model server.", + "progress": { + "phase": "waiting_for_model", + "since": 1762790400000, + "model": "coder", + "reason": "group" + }, + "now": 1762790400000, + "expect": { + "text": "Waiting for coder: its server holds another model now", + "line": "Waiting for coder: its server holds another model now · 0s" + } + }, + { + "about": "Loading.", + "progress": { + "phase": "loading_model", + "since": 1762790400000, + "model": "Gemma 12B" + }, + "now": 1762794125000, + "expect": { + "text": "Loading Gemma 12B", + "line": "Loading Gemma 12B · 1h 02m" + } + }, + { + "about": "Reading the prompt: the percentage is floored.", + "progress": { + "phase": "reading_prompt", + "since": 1762790400000, + "done": 1999, + "total": 3000 + }, + "now": 1762790402000, + "expect": { + "text": "Reading the prompt: 1999 / 3000 tokens (66%)", + "line": "Reading the prompt: 1999 / 3000 tokens (66%) · 2s" + } + }, + { + "about": "Reading the prompt with no counts from the server.", + "progress": { + "phase": "reading_prompt", + "since": 1762790400000 + }, + "now": 1762790402000, + "expect": { + "text": "Reading the prompt", + "line": "Reading the prompt · 2s" + } + }, + { + "about": "Thinking.", + "progress": { + "phase": "thinking", + "since": 1762790400000 + }, + "now": 1762790459999, + "expect": { + "text": "Thinking", + "line": "Thinking · 59s" + } + }, + { + "about": "Writing, the rate with one decimal.", + "progress": { + "phase": "writing", + "since": 1762790400000, + "rate": 12.46 + }, + "now": 1762790404000, + "expect": { + "text": "Writing: 12.5 tok/s", + "line": "Writing: 12.5 tok/s · 4s" + } + }, + { + "about": "Writing before a rate is measured.", + "progress": { + "phase": "writing", + "since": 1762790400000 + }, + "now": 1762790400400, + "expect": { + "text": "Writing", + "line": "Writing · 0s" + } + }, + { + "about": "A tool with what it does.", + "progress": { + "phase": "running_tool", + "since": 1762790400000, + "tool": "bash", + "what": "bun test" + }, + "now": 1762790461000, + "expect": { + "text": "Running bash: bun test", + "line": "Running bash: bun test · 1m 01s" + } + }, + { + "about": "A tool without a title.", + "progress": { + "phase": "running_tool", + "since": 1762790400000, + "tool": "recall" + }, + "now": 1762790401000, + "expect": { + "text": "Running recall", + "line": "Running recall · 1s" + } + }, + { + "about": "An approval, naming the tool.", + "progress": { + "phase": "waiting_for_person", + "since": 1762790400000, + "what": "approval", + "tool": "bash" + }, + "now": 1762790430000, + "expect": { + "text": "Waiting for you: allow bash?", + "line": "Waiting for you: allow bash? · 30s" + } + }, + { + "about": "An approval with no tool named.", + "progress": { + "phase": "waiting_for_person", + "since": 1762790400000, + "what": "approval" + }, + "now": 1762790400000, + "expect": { + "text": "Waiting for you: an approval", + "line": "Waiting for you: an approval · 0s" + } + }, + { + "about": "A question.", + "progress": { + "phase": "waiting_for_person", + "since": 1762790400000, + "what": "question" + }, + "now": 1762790403000, + "expect": { + "text": "Waiting for you: a question", + "line": "Waiting for you: a question · 3s" + } + }, + { + "about": "The plan.", + "progress": { + "phase": "waiting_for_person", + "since": 1762790400000, + "what": "plan" + }, + "now": 1762790403000, + "expect": { + "text": "Waiting for you: the plan", + "line": "Waiting for you: the plan · 3s" + } + }, + { + "about": "Subagents.", + "progress": { + "phase": "waiting_for_subagents", + "since": 1762790400000, + "running": 3 + }, + "now": 1762790490000, + "expect": { + "text": "Waiting for subagents: 3 running", + "line": "Waiting for subagents: 3 running · 1m 30s" + } + }, + { + "about": "Compacting.", + "progress": { + "phase": "compacting", + "since": 1762790400000 + }, + "now": 1762790407000, + "expect": { + "text": "Compacting the conversation", + "line": "Compacting the conversation · 7s" + } + }, + { + "about": "Retrying: `in` counts down from since.", + "progress": { + "phase": "retrying", + "since": 1762790400000, + "reason": "502 Bad Gateway", + "in": 10, + "attempt": 2 + }, + "now": 1762790403000, + "expect": { + "text": "Retrying in 7 s: 502 Bad Gateway", + "line": "Retrying in 7 s: 502 Bad Gateway · 3s" + } + }, + { + "about": "Retrying: never below 0.", + "progress": { + "phase": "retrying", + "since": 1762790400000, + "reason": "overloaded", + "in": 2 + }, + "now": 1762790405000, + "expect": { + "text": "Retrying in 0 s: overloaded", + "line": "Retrying in 0 s: overloaded · 5s" + } + }, + { + "about": "Stopped draws no elapsed time.", + "progress": { + "phase": "stopped", + "since": 1762790400000 + }, + "now": 1762790405000, + "expect": { + "text": "Stopped", + "line": "Stopped" + } + }, + { + "about": "Stopped by the person.", + "progress": { + "phase": "stopped", + "since": 1762790400000, + "reason": "person" + }, + "now": 1762790405000, + "expect": { + "text": "Stopped: by you", + "line": "Stopped: by you" + } + }, + { + "about": "Stopped at the step limit.", + "progress": { + "phase": "stopped", + "since": 1762790400000, + "reason": "steps" + }, + "now": 1762790400000, + "expect": { + "text": "Stopped: step limit", + "line": "Stopped: step limit" + } + }, + { + "about": "Failed.", + "progress": { + "phase": "failed", + "since": 1762790400000, + "reason": "connection refused" + }, + "now": 1762790405000, + "expect": { + "text": "Failed: connection refused", + "line": "Failed: connection refused" + } + } + ] +} diff --git a/harness/conformance/team.json b/harness/conformance/team.json index b791b63..0b2b060 100644 --- a/harness/conformance/team.json +++ b/harness/conformance/team.json @@ -1,5 +1,5 @@ { - "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, and it is the only model there is (spec 3.3.0: other models are subagents.json's). 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 subagent that counts toward the size (and holds a slot) until `task_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/.", + "about": "Teams (team.md, spec 3.2.0; the main agent and its tree, subagents.md §5–§13, spec 3.4.0 — a member's team_add is now the lead's with `parent`, and `offered` lists 3.4.0's tools): 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, and it is the only model there is (spec 3.3.0: other models are subagents.json's). 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 subagent that counts toward the size (and holds a slot) until `task_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.", @@ -19,9 +19,14 @@ { "tools": [ "team_add", + "team_assign", + "team_decide", + "team_findings", "team_read", + "team_replace", "team_send", - "team_stop" + "team_stop", + "team_whoami" ], "setting": true } @@ -43,9 +48,14 @@ { "tools": [ "team_add", + "team_assign", + "team_decide", + "team_findings", "team_read", + "team_replace", "team_send", - "team_stop" + "team_stop", + "team_whoami" ], "setting": true } @@ -113,9 +123,14 @@ { "tools": [ "team_add", + "team_assign", + "team_decide", + "team_findings", "team_read", + "team_replace", "team_send", - "team_stop" + "team_stop", + "team_whoami" ], "setting": true } @@ -200,9 +215,14 @@ { "tools": [ "team_add", + "team_assign", + "team_decide", + "team_findings", "team_read", + "team_replace", "team_send", - "team_stop" + "team_stop", + "team_whoami" ], "setting": true }, @@ -228,7 +248,7 @@ ] }, { - "about": "A member that may not lead has team_send and team_read; one that may lead has all four.", + "about": "A subagent has team_whoami, team_read, team_send, team_findings and team_finish; a sub-manager the same — it adds nothing (spec 3.4.0).", "start": { "min": 1, "max": 4, @@ -279,17 +299,21 @@ }, { "tools": [ + "team_findings", + "team_finish", "team_read", - "team_send" + "team_send", + "team_whoami" ], "setting": true }, { "tools": [ - "team_add", + "team_findings", + "team_finish", "team_read", "team_send", - "team_stop" + "team_whoami" ], "setting": true } @@ -392,7 +416,7 @@ ], "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" + "note": "\nThe team may have 3 to 5 agents, you included.\nWork through subagents from the start: you lead, and they do the work. The person opened 2 slots for them: fill them with team_add.\n" }, { "result": "You lead a team: give this to a member (team_add or team_send).", @@ -458,7 +482,7 @@ ], "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" + "note": "\nThe team may have 2 to 2 agents, you included.\nWork through subagents from the start: you lead, and they do the work. The person opened 1 slots for them: fill them with team_add.\n" } ] }, @@ -632,7 +656,7 @@ ] }, { - "about": "The size is across the whole tree: a member that leads adds within the same max.", + "about": "The size is across the whole tree: subagents placed under a sub-manager count within the same max.", "start": { "min": 1, "max": 4, @@ -653,12 +677,13 @@ }, { "op": "call", - "by": "backend", + "by": "lead", "tool": "team_add", "args": { "name": "db", "role": "owns a part", - "task": "Do the part." + "task": "Do the part.", + "parent": "backend" } }, { @@ -673,12 +698,13 @@ }, { "op": "call", - "by": "backend", + "by": "lead", "tool": "team_add", "args": { "name": "api", "role": "owns a part", - "task": "Do the part." + "task": "Do the part.", + "parent": "backend" } }, { @@ -1538,7 +1564,7 @@ ] }, { - "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).", + "about": "A subagent works; a sub-manager becomes a manager when one is placed under it; stopping them all makes the lead a worker again (min 1).", "start": { "min": 1, "max": 6, @@ -1565,12 +1591,13 @@ }, { "op": "call", - "by": "api", + "by": "lead", "tool": "team_add", "args": { "name": "routes", "role": "owns a part", - "task": "Do the part." + "task": "Do the part.", + "parent": "api" } }, { @@ -1805,12 +1832,13 @@ }, { "op": "call", - "by": "api", + "by": "lead", "tool": "team_add", "args": { "name": "routes", "role": "owns a part", - "task": "Do the part." + "task": "Do the part.", + "parent": "api" } }, { @@ -2009,22 +2037,24 @@ }, { "op": "call", - "by": "api", + "by": "lead", "tool": "team_add", "args": { "name": "routes", "role": "owns a part", - "task": "Do the part." + "task": "Do the part.", + "parent": "api" } }, { "op": "call", - "by": "api", + "by": "lead", "tool": "team_add", "args": { "name": "db", "role": "owns a part", - "task": "Do the part." + "task": "Do the part.", + "parent": "api" } }, { @@ -2396,13 +2426,14 @@ }, { "op": "call", - "by": "api", + "by": "lead", "tool": "team_add", "args": { "name": "routes", "role": "owns a part", "task": "Do the part.", - "goal": "The REST API" + "goal": "The REST API", + "parent": "api" } }, { @@ -2670,7 +2701,7 @@ ] }, { - "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.", + "about": "Stopping is the main agent's: a subagent's team_stop is refused. Stopping a subagent stops its subtree; they leave the team, and their names are free again.", "start": { "min": 1, "max": 8, @@ -2691,33 +2722,36 @@ }, { "op": "call", - "by": "api", + "by": "lead", "tool": "team_add", "args": { "name": "routes", "role": "owns a part", "task": "Do the part.", - "lead": true + "lead": true, + "parent": "api" } }, { "op": "call", - "by": "routes", + "by": "lead", "tool": "team_add", "args": { "name": "auth", "role": "owns a part", - "task": "Do the part." + "task": "Do the part.", + "parent": "routes" } }, { "op": "call", - "by": "api", + "by": "lead", "tool": "team_add", "args": { "name": "db", "role": "owns a part", - "task": "Do the part." + "task": "Do the part.", + "parent": "api" } }, { @@ -2756,7 +2790,7 @@ }, { "op": "call", - "by": "api", + "by": "lead", "tool": "team_stop", "args": { "name": "routes" @@ -2767,12 +2801,13 @@ }, { "op": "call", - "by": "api", + "by": "lead", "tool": "team_add", "args": { "name": "auth", "role": "owns a part", - "task": "Do the part." + "task": "Do the part.", + "parent": "api" } }, { @@ -2817,15 +2852,15 @@ "delivered": {} }, { - "result": "web is not under you: you can stop only the members you lead, directly or through your members.", + "result": "Only the main agent adds, assigns, replaces and stops subagents: ask it (team_send to lead).", "delivered": {} }, { - "result": "api is not under you: you can stop only the members you lead, directly or through your members.", + "result": "Only the main agent adds, assigns, replaces and stops subagents: ask it (team_send to lead).", "delivered": {} }, { - "result": "No agent called ghost in this team. The team: lead, api, routes, auth, db, web.", + "result": "Only the main agent adds, assigns, replaces and stops subagents: ask it (team_send to lead).", "delivered": {} }, { @@ -2900,12 +2935,13 @@ }, { "op": "call", - "by": "api", + "by": "lead", "tool": "team_add", "args": { "name": "routes", "role": "owns a part", - "task": "Do the part." + "task": "Do the part.", + "parent": "api" } }, { diff --git a/harness/loop.json b/harness/loop.json index 6d22c3f..5ab7d67 100644 --- a/harness/loop.json +++ b/harness/loop.json @@ -84,7 +84,7 @@ } }, "team": { - "about": "Teams (spec 3.2.0, team.md; which model a member runs on and when a team is offered: spec 3.3.0, subagents.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.", + "about": "Teams (spec 3.2.0, team.md; which model a member runs on and when a team is offered: spec 3.3.0, subagents.md; the main agent, the tree, permissions and a subagent's life: spec 3.4.0, subagents.md §5–§13). 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, conformance/delegation.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, @@ -92,17 +92,24 @@ "cap": 32 }, "names": { - "about": "A member's name: unique in the team; `lead` is the lead's, `all` addresses everyone.", + "about": "A subagent's name: unique among the subagents that have not ended; `lead` is the main agent's, `all` addresses everyone, `parent` (3.4.0) the sender's parent.", "pattern": "^[a-z][a-z0-9-]{0,31}$", "lead": "lead", "reserved": [ "all", - "lead" - ] + "lead", + "parent" + ], + "parent": "parent", + "id_prefix": "s" }, "marks": { "message": "[Team message from {{from}} to {{to}}:]", - "report": "[Team report from {{from}} to {{to}}:]" + "report": "[Team report from {{from}} to {{to}}:]", + "assignment": "[Team assignment from {{from}} to {{to}}:]", + "request": "[Team request {{id}} from {{from}} to {{to}}:]", + "decision": "[Team decision on {{id}} from {{from}} to {{to}}:]", + "context_full": "[Team notice to {{to}}: your context is full]" }, "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_request: no model can serve as a subagent any more (subagents.md §4) — the session's model's capacity changed after the prompt was made.", @@ -153,25 +160,106 @@ "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}}]" + "ended": "[ended: {{reason}}]", + "only_main": "Only the main agent adds, assigns, replaces and stops subagents: ask it (team_send to lead).", + "not_manager": "{{name}} is not a sub-manager: place a subagent under the main agent (lead) or under one added with lead: true.", + "wider": "A subagent's mode cannot be looser than its parent's ({{parent_mode}}).", + "request_not_narrower": "Request options go with a mode stricter than the parent's: without one there is nothing to ask for.", + "request_kind": "{{kind}} is not a request option: use read, write or execute.", + "requestable": "Your mode does not allow this, but the main agent may: ask for it with team_request (the tool, the pattern, why), then try again once it allows.", + "not_delegated": "Your mode does not allow this, and you may not ask for it: tell your parent what you need.", + "not_requestable": "You may not ask for {{tool}}: your request options are {{kinds}}.", + "requested": "Asked the main agent ({{id}}): you are told its decision. Carry on with what you can meanwhile.", + "no_request": "No open request {{id}}.", + "decided": "{{decision}} {{id}} for {{name}}.", + "allowed": "Allowed", + "refused": "Refused", + "decision_allowed": "Allowed: {{tool}} {{pattern}}. Try it again now.", + "decision_refused": "Refused: {{tool}} {{pattern}}.", + "no_parent": "You are the main agent: you have no parent.", + "finished": "Reported to {{to}}. Your task is done: stop here.", + "has_ended": "{{name}} has ended: replace it (team_replace) or add another.", + "cycle": "{{name}} cannot go under {{parent}}: that is under it.", + "assigned": "Assigned to {{name}}: it is told now.", + "replaced": "Replaced {{name}}: {{id}} takes its place, briefed with its task and findings. The team is {{size}} of {{max}} agents.", + "replace_brief": "{{task}}\n\nYou take over from {{name}} ({{id}}), which ended. What it found:\n{{findings}}", + "no_findings": "(no findings)", + "noted": "Noted (finding {{n}}).", + "not_yours_findings": "You may read only your own findings and those of the subagents under you.", + "context_full": "Save what you found with team_findings, then tell your parent with team_send that you are gone and what is left undone. This is your last turn: at most {{steps}} steps.", + "gone": "{{name}} ({{id}}) ran out of context and has ended. Its findings: team_findings with of {{name}}.", + "request_body": "{{tool}} {{pattern}} — {{why}}" }, "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.", + "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. Spec 3.4.0: the row adds the id, the mode (with request options) and the context use; open slots follow the tree, ended subagents after them.", "tree": "Team ({{size}} of {{max}} agents):", - "agent": "{{indent}}- {{name}} — {{role}} · {{state}} · {{model}} · {{last}}", - "lead_role": "leads the team", + "agent": "{{indent}}- {{name}} ({{id}}) — {{role}} · {{state}} · {{model}} · {{mode}} · {{context}} · {{last}}", + "lead_role": "the main agent", "states": { "working": "working", "waiting": "waiting for a slot", "finished": "finished", - "idle": "idle" + "idle": "idle", + "paused": "paused: context full", + "done": "done", + "out_of_context": "out of context", + "stopped": "stopped" }, "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}}." + "more": "More after #{{last}}: team_read with since {{last}}.", + "slots": "Open slots: {{n}} — fill them with team_add.", + "ended": "Ended:", + "ended_agent": "- {{name}} ({{id}}) — {{role}} · {{state}} · {{findings}} findings", + "context": "{{percent}}% context", + "request": "+ {{kinds}}" + }, + "main_only": { + "about": "Spec 3.4.0 (subagents.md §6, §8): the tools only the main agent has; a subagent's call to one, or to task, answers texts.only_main.", + "tools": [ + "team_add", + "team_assign", + "team_decide", + "team_replace", + "team_stop" + ] + }, + "context": { + "about": "Spec 3.4.0 (subagents.md §9): a subagent does not compact. When its context reaches pause_at of its model's window it is paused and given one final turn of at most final_steps steps, whose user message is the context_full mark and texts.context_full.", + "pause_at": 0.85, + "final_steps": 8 + }, + "request": { + "about": "Spec 3.4.0 (subagents.md §7): the kinds a request option may name — risk classes — and the ids of requests, r1, r2, … per session.", + "kinds": [ + "read", + "write", + "execute" + ], + "id_prefix": "r" + }, + "whoami": { + "about": "Spec 3.4.0 (subagents.md §8): team_whoami's answer, one line each; `request` only with request options, `parent` only for a subagent. {{percent}} is floor(used × 100 / window).", + "lines": { + "who": "You are {{name}} ({{id}}): {{role}}.", + "parent": "Your parent: {{parent}}.", + "task": "Your task: {{task}}", + "mode": "Your mode: {{mode}}.", + "request": "You may ask the main agent for: {{kinds}} (team_request).", + "model": "Your model: {{model}}.", + "limits": "Limits: the team is {{size}} of {{max}} agents; at most {{steps}} steps a turn; {{spawn}}", + "spawn_yes": "you add subagents.", + "spawn_no": "only the main agent adds subagents.", + "context": "Context: {{used}} of {{window}} tokens ({{percent}}%)." + } + }, + "findings": { + "about": "Spec 3.4.0: team_findings read's answer — the header, then one line per finding, oldest first; none: texts.no_findings after the header.", + "header": "Findings of {{name}} ({{id}}):", + "item": "{{n}}. {{text}}" } }, "context": { @@ -239,12 +327,116 @@ "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_lead": "Work through subagents from the start: you lead, and they do the work. The person opened {{members}} slots for them: fill them with team_add.", "team_one": "The person allows one agent: work alone; the team tools and task are unavailable.", "team_own": "This model serves one request at a time, so subagents cannot run on it: they need another model, on another server, that the person names in this chat.", "team_stopped": "The person stopped {{names}}; they have left the team.", "empty": "(none)" }, "mcp_wait_ms": 3000 + }, + "progress": { + "about": "Spec 3.4.0 (progress.md): one fixed set of phases every client shows the same way. `texts` are the shared i18n keys progress. (a variant: progress..); `line` is drawn for every phase but stopped and failed; elapsed in whole seconds (5s, 1m 05s, 1h 02m). conformance/progress.json.", + "phases": [ + "queued", + "waiting_for_model", + "loading_model", + "reading_prompt", + "thinking", + "writing", + "running_tool", + "waiting_for_person", + "waiting_for_subagents", + "compacting", + "retrying", + "stopped", + "failed" + ], + "fields": { + "queued": { + "ahead": "int?" + }, + "waiting_for_model": { + "model": "string", + "reason": "busy|slots|group" + }, + "loading_model": { + "model": "string" + }, + "reading_prompt": { + "done": "int?", + "total": "int?" + }, + "thinking": {}, + "writing": { + "rate": "number?" + }, + "running_tool": { + "tool": "string", + "what": "string?" + }, + "waiting_for_person": { + "what": "approval|question|plan", + "tool": "string?" + }, + "waiting_for_subagents": { + "running": "int" + }, + "compacting": {}, + "retrying": { + "reason": "string", + "in": "int", + "attempt": "int?" + }, + "stopped": { + "reason": "person|steps|budget?" + }, + "failed": { + "reason": "string" + } + }, + "texts": { + "queued": "Queued", + "queued.ahead": "Queued: {{ahead}} ahead", + "waiting_for_model": "Waiting for {{model}}: {{reason}}", + "loading_model": "Loading {{model}}", + "reading_prompt": "Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)", + "reading_prompt.bare": "Reading the prompt", + "thinking": "Thinking", + "writing": "Writing: {{rate}} tok/s", + "writing.bare": "Writing", + "running_tool": "Running {{tool}}: {{what}}", + "running_tool.bare": "Running {{tool}}", + "waiting_for_person": "Waiting for you: {{what}}", + "waiting_for_subagents": "Waiting for subagents: {{running}} running", + "compacting": "Compacting the conversation", + "retrying": "Retrying in {{in}} s: {{reason}}", + "stopped": "Stopped", + "stopped.reason": "Stopped: {{reason}}", + "failed": "Failed: {{reason}}" + }, + "reason": { + "busy": "it serves one request at a time and is answering another", + "slots": "all its slots are in use", + "group": "its server holds another model now" + }, + "what": { + "approval": "allow {{tool}}?", + "approval.bare": "an approval", + "question": "a question", + "plan": "the plan" + }, + "stopped_reason": { + "person": "by you", + "steps": "step limit", + "budget": "budget" + }, + "line": "{{text}} · {{elapsed}}", + "no_elapsed": [ + "stopped", + "failed" + ], + "what_max": 80, + "pace_per_second": 4 } } diff --git a/harness/permission/defaults.json b/harness/permission/defaults.json index 535f27b..5450ac7 100644 --- a/harness/permission/defaults.json +++ b/harness/permission/defaults.json @@ -30,6 +30,13 @@ "team_send": "allow", "team_read": "allow", "team_stop": "allow", + "team_whoami": "allow", + "team_finish": "allow", + "team_findings": "allow", + "team_request": "allow", + "team_decide": "allow", + "team_assign": "allow", + "team_replace": "allow", "web_search": "allow", "memory": "allow", "session_search": "allow", diff --git a/harness/progress.md b/harness/progress.md new file mode 100644 index 0000000..ba3c4ca --- /dev/null +++ b/harness/progress.md @@ -0,0 +1,107 @@ +# Progress + +Spec 3.4.0. **One way to say what a session is doing**, the same in every client — the web UI, +Desktop, the CLI, Android — and for every agent in it, the main agent and each subagent alike. +Before 3.4.0 each client invented its own subset from a free `status` text; that text stays, for +compatibility, and beside it goes a structured **progress** object with one of a fixed set of +**phases**. The texts are `loop.json` `progress`; the cases `conformance/progress.json`. + +## 1. The object + +```json +{ "phase": "running_tool", "since": 1762790400000, "tool": "bash", "what": "bun test" } +``` + +- **`phase`** — one of the thirteen names below, never another. A session doing nothing has no + progress: `null`. +- **`since`** — when this phase began, Unix time in **milliseconds** (an integer). A change of the + phase's data (tokens read, the rate) keeps `since`; a new phase, or the same phase for another + thing (another tool call, another model), starts a new one. +- The phase's **data**, as listed: every field named here, with these names and these types. A + field that is not known is left out (never `null`, never a guess), and the text says less. + +## 2. The phases + +In the order a turn usually meets them. `{{x}}` in a text is the field `x`; the texts are +`loop.json` `progress.texts`, word for word, and they are the shared i18n keys +`progress.` (and `progress..`, `progress.reason.`, +`progress.what.`) in every client's translations. + +| Phase | Data | Text (`progress.texts`) | +|---|---|---| +| `queued` | `ahead?: int` — turns before it | `Queued` · with `ahead`: `Queued: {{ahead}} ahead` | +| `waiting_for_model` | `model: string` (its display name, else its id); `reason: "busy" \| "slots" \| "group"` | `Waiting for {{model}}: {{reason}}` | +| `loading_model` | `model: string` | `Loading {{model}}` | +| `reading_prompt` | `done: int`, `total: int` — prompt tokens processed of all | `Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)` · without `total`: `Reading the prompt` | +| `thinking` | — | `Thinking` | +| `writing` | `rate?: number` — tokens a second, as measured over this reply | `Writing: {{rate}} tok/s` · without `rate`: `Writing` | +| `running_tool` | `tool: string` (the tool's name); `what?: string` — the call's short title (the command, the path, the query), one line, at most 80 characters | `Running {{tool}}: {{what}}` · without `what`: `Running {{tool}}` | +| `waiting_for_person` | `what: "approval" \| "question" \| "plan"`; `tool?: string` (approval: the tool asked about) | `Waiting for you: {{what}}` | +| `waiting_for_subagents` | `running: int` — subagents working or waiting now | `Waiting for subagents: {{running}} running` | +| `compacting` | — | `Compacting the conversation` | +| `retrying` | `reason: string` — the server's error, one line; `in: int` — seconds until the retry, counted down by the client from `since`; `attempt?: int` | `Retrying in {{in}} s: {{reason}}` | +| `stopped` | `reason?: "person" \| "steps" \| "budget"` | `Stopped` · with `reason`: `Stopped: {{reason}}` | +| `failed` | `reason: string` — the error, one line | `Failed: {{reason}}` | + +The fixed words a field stands for: + +- `reason` of `waiting_for_model` (`progress.reason`): `busy` — *it serves one request at a time + and is answering another*; `slots` — *all its slots are in use*; `group` — *its server holds + another model now*. +- `what` of `waiting_for_person` (`progress.what`): `approval` — *allow {{tool}}?* (*an approval* + without `tool`); `question` — *a question*; `plan` — *the plan*. +- `reason` of `stopped` (`progress.stopped_reason`): `person` — *by you*; `steps` — *step limit*; + `budget` — *budget*. +- `percent` is `floor(done × 100 / total)`; numbers are written as integers, `rate` with one + decimal (`12.5`), in the client's own number format. + +## 3. The line a client draws + +**`{{text}} · {{elapsed}}`** (`progress.line`) for every phase but `stopped` and `failed`, which +draw their text alone. `elapsed` is now − `since`, written in whole seconds as a ticking clock writes durations +(`5s`, `1m 05s`, `1h 02m`), and kept moving by the client at least once a second while the +phase lasts. `retrying`'s `in` is likewise counted down from `since` (never below 0). + +Every client draws the same line for the same object — the same words, from the same keys — and +its own look around it (a spinner, a colour, a bar for `reading_prompt`). Where a line has no room +(a phone's notification, a tree row) the text alone, without `· elapsed`, is enough; nothing else +is shortened. + +## 4. When each phase holds + +What decides the phase, so that two implementations agree on the same moment: + +- `queued` — a prompt waits behind a turn that runs (the device's turn queue; the server's per + chat). `waiting_for_model` — the turn may start but its model may not take it yet: a one-request + server answering another request (`busy`), every slot of the model held (`slots`; `team.md` §8), + another model of its one-model server in use (`group`). `loading_model` — only where the + implementation is told the model is being loaded (the server sees it; a device that cannot tell + skips it). +- `reading_prompt` — the request is sent and no output has come; with `done`/`total` from the + server's prompt-progress frames (`quirks.json` `prompt-progress`), without them where none come. +- `thinking` — reasoning streams; `writing` — reply text (or a tool call's arguments) streams. +- `running_tool` — from a tool call's start to its end; with calls in parallel, the one that + started first. `waiting_for_person` — an approval card, a question or a plan to approve is open + for this agent. `waiting_for_subagents` — the agent's turn waits on subagents: a `task` call in + the foreground, or the main agent with nothing to do but its subagents' reports (`subagents.md` + §9). +- `compacting` — a compaction runs. `retrying` — a failed request will be sent again after a wait. +- `stopped` / `failed` — the turn ended other than by finishing: stopped by the person, a step limit + or a budget; failed with an error. They are the last progress of that turn and stay until the + next one begins; a turn that finished normally ends with `null`. + +## 5. Where it is sent + +- **`/api/v2` (LLeMbas)**: every frame of a chat's stream that carries the `status` text carries + `progress` beside it — the object, or `null` — and a frame is sent when the progress changes: + a new phase at once, a data-only change (`done`, `rate`) at most four times a second. `status` + stays as it was, its text unchanged, for clients that do not read `progress`. +- **The link (`acp.md`, capability `progress`)**: the `phase` event `{ progress, agent? }` — + `agent` the subagent's id (`subagents.md` §5) when the progress is a subagent's, absent for the + main agent's — under the same pacing; `progress` in `_lembas/session/status`, so a client + reattaching draws it at once. The device sends it; the server passes it on in `/api/v2` as its + own. +- **The subagent tree** (`subagents.md` §12): every agent carries its current `progress`. + +A client that reads `progress` draws it and ignores `status`; one that does not, draws `status` as +before. diff --git a/harness/prompts/blocks/team_lead.md b/harness/prompts/blocks/team_lead.md index ea4980a..634cf9e 100644 --- a/harness/prompts/blocks/team_lead.md +++ b/harness/prompts/blocks/team_lead.md @@ -1,12 +1,14 @@ - -# 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. + +# Leading subagents +You are the main agent of this session. You can add subagents with team_add: agents that work in the background, each on the part of the work it owns, while you lead. Only you add, assign, replace and stop them. A small job you may do yourself. Once you have a subagent 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 subagent owns a thing from start to finish. +- Name each subagent after what it owns and give it a position (`role`). 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 tree flat while it is small — up to six or eight subagents, each reporting to you. Beyond that, add a sub-manager for each area (`lead: true`) and place that area's subagents under it (`parent`); it directs them and reports to you, and you still add and stop every one. +- A subagent's mode is yours by default and never looser. Give a stricter one where the person wants less done unasked; with it you may give `request` options, and you decide its requests with team_decide — allow only what the task needs. +- Keep within the size the person allows, which a session note tells you; when it says slots are open, fill them. Stop a subagent whose part is done or no longer needed, to make room. +- Coordinate through the room: team_send to the subagent who owns a thing, `all` only for what everyone needs. team_read shows the tree and the room when you need to look; nothing from it reaches you unasked except reports, requests and messages addressed to you. +- A subagent whose context fills saves its findings and ends. Read them with team_findings, then replace it (team_replace) if its part is not done. +- Step in when a subagent is stuck, when two disagree, or when the plan has to change: decide, and tell those it concerns (team_assign for new work). +- 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. diff --git a/harness/prompts/blocks/team_member.md b/harness/prompts/blocks/team_member.md index 3032b49..43f32e9 100644 --- a/harness/prompts/blocks/team_member.md +++ b/harness/prompts/blocks/team_member.md @@ -1,14 +1,16 @@ - -You are "{{name}}", a member of a team, and you report to "{{manager}}". -Your part: {{role}} + +You are "{{name}}" ({{id}}), a subagent of this session, and you report to "{{manager}}". +Your position: {{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. +You work for your parent on the part you own, and you cannot ask the person anything: ask your parent with team_send to `parent`. Only the main agent (`lead`) adds, assigns and stops subagents. Other subagents own other parts. Message the one who owns a thing when you need it from them, and `all` only for what everyone needs; messages to you arrive as you work. team_read shows the tree and the room; team_whoami shows who you are, your mode and how full your context is. -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}} +Your context is limited and you do not compact: write what you find with team_findings as you go. When your context fills you are told, and you have one last turn to save your findings and tell your parent. + +When your task is done, call team_finish with your report to "{{manager}}" — make it complete on its own: the result, then the evidence (file:line), then anything you could not settle. A final reply without it is also your report, and a message may then wake you again.{{read_only}} {{lead}} diff --git a/harness/prompts/blocks/team_submanager.md b/harness/prompts/blocks/team_submanager.md new file mode 100644 index 0000000..ed50664 --- /dev/null +++ b/harness/prompts/blocks/team_submanager.md @@ -0,0 +1,8 @@ + +# Directing an area +You are a sub-manager: the main agent places subagents under you, and you direct them. You do not add, assign, replace or stop subagents — ask the main agent with team_send to `lead` when your area needs one more, another task for one, or one stopped. You manage and no longer do the work: tools that change things are refused to you, and reading stays yours, for review. +- Split your area's work among the subagents under you, tell each what it owns with team_send, and keep them from overlapping. +- Their reports come to you. Review each against what you asked — read the work itself where it matters — and send back what is not done. +- Read the findings of a subagent under you with team_findings, especially of one that ran out of context; tell the main agent whether it needs replacing. +- When your area is done, call team_finish with your report to the main agent. diff --git a/harness/prompts/index.json b/harness/prompts/index.json index 9f330d3..8076bcc 100644 --- a/harness/prompts/index.json +++ b/harness/prompts/index.json @@ -251,16 +251,23 @@ "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", + "why": "spec 3.2.0, 3.4.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)", + "role": "its position, one line (team_add's role)", + "manager": "its parent: lead, or the sub-manager it was placed under", "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" + "lead": "for a sub-manager (added with `lead: true`): the whole of blocks/team_submanager.md (spec 3.4.0; before, blocks/team_lead.md); otherwise empty", + "id": "the subagent's id (s1, s2, …; spec 3.4.0)" } + }, + "blocks/team_submanager.md": { + "scope": "shared", + "llembas": [], + "llembas_text": "identical", + "why": "spec 3.4.0: LLeMbas reads it from the vendored spec (harness_spec.prompt), not from a fragment" } } } diff --git a/harness/subagents.md b/harness/subagents.md index c206dbf..59640a8 100644 --- a/harness/subagents.md +++ b/harness/subagents.md @@ -1,6 +1,6 @@ # Subagents -Spec 3.3.0. **Which model a subagent runs on, and when subagents are offered at all.** A +Spec 3.3.0; §5–§13 spec 3.4.0. **Which model a subagent runs on, and when subagents are offered at all.** A **subagent** is any agent a session's agent starts to work for it: a `task` subagent (in the foreground or as a background job) and a team's member (`team.md`) alike. The same rules in a chat on the server and in an agent session on a device, in both projects. The texts are `loop.json` @@ -88,3 +88,263 @@ before 3.3.0 for a single model. What counts as **usable** is each project's: LLeMbas — the models the person may use in the chat's picker; the CLI — the models of the server it is logged in to. + +--- + +# Subagents v2 — the main agent and its tree + +Spec 3.4.0. **How subagents are organised, what each may do, and how each ends.** The owner's model: +one permanent **main agent** per session, which alone makes subagents and places them in a tree; +permissions are inherited and never widened; a subagent is temporary and ends with its task or its +context. Sections 5–13 **supersede `team.md` where they say something different** — who adds a +subagent (§6), who stops one (§10), the states (§9), what a member is told (§13) — and keep the +rest of it: the size setting and its counting (§2–§3), the room and who receives what (§5), +delivery and waking (§7), a manager's refusals (§6 there), capacity and waiting for a slot (§8). +The tool names `team_*` stay (nothing stored or trained breaks); `task` and `report` are unchanged +for a one-off subagent. The texts are `loop.json` `team` (3.4.0 additions marked there); the cases +`conformance/team.json` (the tree, the tools, the lifecycle) and `conformance/delegation.json` +(permissions). + +## 5. Words + +- **The main agent** is the session's own agent: the chat's agent on LLeMbas, the session's in the + CLI. It is **permanent**: it compacts and never ends. In the room and in every tool it is called + `lead` (the name is kept from 3.2.0); a person reads it as *the main agent*. +- **A subagent** is an agent the main agent added (`team_add`). It has: + - **`id`** — `s1`, `s2`, … in the order the session's subagents were added, never reused in the + session (a replacement gets a new one); + - **`name`** — the main agent's choice (`team.names.pattern`), unique among the subagents that + have not ended, `lead`, `all` and `parent` reserved; + - **`position`** — its designation, one line, given by the main agent (`team_add`'s `role`: + *the API's tests*, *reviews the parser*); + - **`parent`** — the main agent, or a **sub-manager** the main agent placed it under; + - **`task`** — its brief, as given (and as replaced by `team_assign`); + - **`mode`** and **`request`** — its permission mode and its request options (§7); + - **`model`** — `subagents.md` §2–§3, unchanged. +- **A sub-manager** is a subagent added with `lead: true`. It **directs** the subagents the main + agent places under it — they report to it, it messages them, it reviews their work — and it does + **not** add, assign, replace or stop subagents: only the main agent does (§6). Like every manager + it does not do the work itself (`team.md` §6, unchanged). +- **The tree**: the main agent at the root, every subagent under its parent, in the order added. + **Under** an agent and **its subtree** keep their `team.md` meaning. + +## 6. Only the main agent makes subagents + +- **`team_add`, `team_assign`, `team_replace`, `team_stop` and `team_decide`** are the main agent's. + A subagent's call to any of them is refused, word for word, with `team.texts.only_main`: + + > Only the main agent adds, assigns, replaces and stops subagents: ask it (team_send to lead). + + A subagent's tool list never has `task` (a one-off subagent is the main agent's too), and a call + to it by a former name is refused with the same text. +- **`team_add`** takes, beside 3.2.0's `name`, `role` (the position), `task`, `goal`, `lead`, + `agent`, `model`: **`parent`** — the name of a sub-manager to place it under (default: the main + agent; refused with `team.texts.not_manager` for a subagent that is not a sub-manager, and + `unknown` for no such name), **`mode`** — its mode (default: its parent's; §7), **`request`** — + its request options (§7). Its refusals, in order: `unavailable`, `only_main`, then 3.2.0's (`all_name`, + `bad_name`, `taken`, `no_goal`, `full`), then `unknown` and `not_manager` for `parent`, then the mode's (`wider`, `request_not_narrower`, + `request_kind`), then the model's (§2–§3). `parent` and the reserved names are refused as + `taken`. +- **The person** can tell the main agent to spawn — it is the main agent's call, as any — or set + **Min above 1**: then **`min − 1` slots** exist from the start of the main agent's reply (the + first one after the setting was told, `team.md` §10). A **slot** is a place in the tree for a + subagent not added yet: it is shown (§12) and `team_read` lists it, it is **not** an agent and + does not count toward the size, and each subagent added fills one. Open slots are + `max(0, min − 1 − subagents that have not ended)`. The main agent is told to use them in its + note (`context.note.team_lead`, which 3.4.0 words as slots), and is a manager from the start + (`team.md` §6). +- **Min/Max** (3.3.0) are unchanged: they count the main agent; 1 is the main agent alone. The + model rules (§2–§3) are unchanged. + +## 7. Permissions: inherited, never widened + +**A subagent's mode is its parent's or stricter** — `permission/modes.json` `order`, loosest first: +`auto`, `edit`, `manual`, `plan`. The main agent's mode is the session's. + +- `team_add`'s and `team_assign`'s `mode`, looser than the parent's, is refused, word for word, + with `team.texts.wider`: + + > A subagent's mode cannot be looser than its parent's ({{parent_mode}}). + + When the parent's own mode becomes stricter during the session (the person switches the main + agent to manual), every subagent looser than its parent is made as strict as it, down the tree, + at once; a looser switch changes no subagent. +- **Request options** (`request`: a list of the risk classes `read`, `write`, `execute`) may be + given **only with a mode stricter than the parent's** — otherwise refused with + `team.texts.request_not_narrower` ("Request options go with a mode stricter than the parent's: + without one there is nothing to ask for."); an unknown kind with `team.texts.request_kind`. They + let the subagent **ask the main agent** for a call of that class that its own mode does not + allow; **the main agent is the check**. + +**Who decides a subagent's call.** The hardline and the deny rules first, as for any call. Then: + +1. The call is evaluated in the **subagent's mode**, with the session's rules and approvals and + the subagent's own grants (below). **Allow** — it runs. **Deny** (a rule, the hardline, plan + mode's refusal) — refused, as always. +2. **Ask** — the call is evaluated in the **main agent's mode** (the session's rules and + approvals, no grants): + - **Allow** — the main agent could allow it: the subagent was narrowed. If the call's risk class + is in its `request`, refused with `team.texts.requestable` — it asks with `team_request`, the + main agent decides with `team_decide`, and once allowed the same call runs. Otherwise refused + with `team.texts.not_delegated`: + + > Your mode does not allow this, and you may not ask for it: tell your parent what you need. + + - **Ask** — **nobody in the tree can allow it: it goes to the person**, as an approval card that + names the subagent (its name and position), the same card and the same answers as the main + agent's own; unattended, refused as `modes.json` `unattended` says. So in an all-Edit tree a + subagent's `bash` reaches the person — not the main agent, which could not allow it either. + - **Deny** — refused with that reason. + + A sub-manager is never asked: it may not allow more than the main agent, and the main agent is + the check. + +**`team_request`** (a subagent with request options; its tool list has it only then): `tool`, the +permission `pattern` it needs (as a rule writes it: `git push *`, a path) and `why`. Refused with +`team.texts.not_requestable` when the tool's risk class is not in its options. Posted to the main +agent as a request, under its own mark (`team.marks.request`), the body `team.texts.request_body`, +with an id (`r1`, `r2`, … per session); the answer `team.texts.requested` says it waits for the main agent's decision. + +**`team_decide`** (the main agent): `id`, `allow` (a boolean), `note`. Allowed — the subagent gets +a **grant**: the rule `{ permission: , pattern, action: allow }` for itself alone, for +the rest of its life; **a grant only ever applies where the main agent's own mode would allow the +call** (step 2's evaluation, done again at the call), so it can never widen past the main agent. +Either way the subagent is told, under `team.marks.decision`: `team.texts.decision_allowed` or +`decision_refused`, then the note after a blank line. The answer to the main agent is +`team.texts.decided` (`decision` being `allowed` or `refused`). Refused: +`team.texts.no_request` for an id that is not an open request. + +## 8. What each agent has + +| Tools | Main agent | Sub-manager | Subagent | +|---|---|---|---| +| `team_whoami`, `team_read`, `team_send`, `team_findings` | ✓ | ✓ | ✓ | +| `team_finish` | — | ✓ | ✓ | +| `team_request` | — | with request options | with request options | +| `team_add`, `team_assign`, `team_replace`, `team_stop`, `team_decide` | ✓ | — | — | + +Fixed when the prompt is made (the main agent's) or when the subagent is added (`context.md` §10, +rule 4). What each does: + +- **`team_whoami`** — who the caller is: `team.whoami`, one line each, in its order — id, name, position, parent, task, mode and + request options, model, the limits (the size and Max, the steps a turn may take, whether it may + add subagents) and its **context use** (tokens in its context now, of its model's window, and the + percentage). For the main agent: the same, its position *the main agent*, without the parent and task lines. +- **`team_read`** — the roster: the tree (every agent and open slot, §12's fields) and the room, + as in 3.2.0 (`team.read`; 3.4.0's row adds the id, the mode and the context use). +- **`team_send`** — a message `to` names, `all`, or **`parent`** (3.4.0): the caller's parent + (refused for the main agent with `team.texts.no_parent`). Otherwise 3.2.0. +- **`team_finish`** — `report`: the subagent's task is done. The report goes to its parent (the + report mark, as a final reply's), and the subagent **ends** (§9): its turn stops after this call, + it leaves the size, and its findings and transcript stay. +- **`team_findings`** — `write` (`text`): a finding kept for the caller, numbered from 1; `read` + (`of`: a name or an id, default the caller): the findings of the caller, or of an agent in its + subtree (the main agent: anybody's), ended ones included — as `team.findings` writes them. A + name that has been used more than once means its latest holder; an id is exact. Otherwise refused with + `team.texts.not_yours_findings`. The person reads every agent's (§12). +- **`team_assign`** — `name`, `task`, and optionally `role` (a new position), `parent` (another + sub-manager, or `lead`), `mode` (§7): a new task for a subagent that has not ended. Its task + becomes this one; it is told under `team.marks.assignment`, and a finished one is woken. Refused: + `unknown`, `team.texts.ended` for one that has ended, `not_manager`, `wider`; a move under its + own subtree with `team.texts.cycle`. +- **`team_replace`** — `name`, and optionally `task`: the subagent ends (stopped, if it has not + already), and a new one takes its place — the same name, position, parent, mode, request options + and model, a new id — briefed with `task` (default: the old task) followed by the old one's + findings (`team.texts.replace_brief`). Its subagents move under the new one. Refused: `unknown`. +- **`team_stop`** — the main agent's only (3.2.0 let a manager stop its subtree); otherwise 3.2.0. +- **`team_decide`** — §7. + +## 9. A subagent's life + +States (`team.read.states`): **`working`** (a turn runs), **`waiting`** (it has work and waits for +a slot, `team.md` §8), **`finished`** (its last turn ended with a final reply, which was its report; +a message wakes it — 3.2.0), **`paused`** (its context is full; its last turn runs), and the ends: +**`done`** (`team_finish`), **`out_of_context`** (its last turn after `paused` ended), **`stopped`** +(the main agent's `team_stop`, a replacement, or the person). The main agent is `working`, +`waiting` or `idle`. + +**An ended subagent** — `done`, `out_of_context`, `stopped` — leaves the size and its slot frees; +its name may be used again; it keeps its id, and the tree keeps it (§12) with its findings and +transcript for the rest of the session. Messages to it by name are refused as `unknown`. + +**When its context fills.** A subagent does not compact. When its context reaches +`team.context.pause_at` of its model's window (default 0.85, the fraction compaction would start +at), after the step that took it there: + +1. it is **`paused`**: nothing new reaches it — a message to it is posted in the room and not + delivered; whoever replaces it reads it with `team_read`; +2. it is given **one final turn**, at most `team.context.final_steps` steps (8), whose user message + is `team.texts.context_full`, under its own mark: save what it found with `team_findings`, and + tell its parent with `team_send` that it is gone and what is left; +3. when that turn ends it is **`out_of_context`**, and its parent is sent `team.texts.gone` — then, + after a blank line, that turn's final reply if it gave one — as its report (the report mark): the name, that it ran out of context, and how to read its + findings. **The parent decides** — the main agent replaces it (`team_replace`) or uses the + findings; a sub-manager tells the main agent. + +**The main agent is permanent**: it compacts as every session does (`context.md` §7) and never +ends; deleting the session ends every subagent. + +**Waiting for subagents.** The main agent's turn may end while subagents work; their reports wake +it (3.2.0 §7). Its progress is `waiting_for_subagents` (`progress.md`) while its turn waits on +them — a foreground `task` — and the session shows the subagents' own phases in the tree. + +## 10. Stopping + +Only the main agent stops (`team_stop`, 3.2.0's refusals but `not_yours`, which no longer arises), +and **the person** (§12) — said to the main agent as a note (`context.note.team_stopped`). A +stopped subagent's subtree is stopped with it, unless the main agent moved it first. A sub-manager +that wants a subagent of its own stopped asks the main agent. + +## 11. Transcripts and findings + +Every subagent's conversation is kept — its brief, what it wrote, every tool call with its output — +for as long as the session: **viewable read-only by the person, live while it runs and after it +ended**. Nobody types into it; the person speaks to a subagent only through the main agent (or by +stopping it). Findings (§8) are kept the same way, readable by its parent, the main agent, and the +person. + +## 12. What clients show + +**The tree** — the main agent, every subagent under its parent, open slots, and ended subagents +(after the live ones, dimmed, with how each ended): + +| Field | Shown as | +|---|---| +| `name`, `id`, `position` | `parser` (`s2`) — *reviews the parser* | +| `state` | `team.read.states`, its word | +| `model` | display name | +| `mode` | the mode's name, and request options as `+ write` etc. | +| `context` | `{used, window}` → `42%` (a bar where there is room) | +| `progress` | its current phase line (`progress.md` §3) | +| messages | the room's messages to and from it: how many unread by the person, the last one's first line | + +- **Web UI, Desktop and the CLI** show the whole tree, the room, each subagent's findings and its + context (the CLI: `/context `), and open any subagent's transcript read-only, live and after + it ended; the person can stop any subagent there. +- **Android** shows a reduced list — name, position, state, progress line, context % — and opens a + transcript read-only. + +**The data**, the same in `/api/v2` (LLeMbas: a chat's stream carries the tree as it changes) and on +the link (`acp.md`, capability `tree`): + +```json +{ "min": 1, "max": 4, "size": 2, "open_slots": 0, + "agents": [ + { "id": "lead", "name": "lead", "position": "the main agent", "parent": null, "state": "working", + "model": "qwen-27b", "mode": "edit", "request": [], "manager": true, + "context": { "used": 41200, "window": 131072 }, "progress": { "phase": "thinking", "since": 1762790400000 } }, + { "id": "s1", "name": "parser", "position": "reviews the parser", "parent": "lead", "state": "working", + "model": "qwen-27b", "mode": "manual", "request": ["write"], "manager": false, "task": "…", + "context": { "used": 9100, "window": 131072 }, "progress": { "phase": "running_tool", "since": 1762790401000, "tool": "read", "what": "src/parse.ts" }, + "findings": 2, "messages": 3 } + ], + "ended": [ { "id": "s0", "name": "scout", "position": "…", "parent": "lead", "state": "done", "ended": 1762790300000, "findings": 1 } ] } +``` + +## 13. What each is told + +- The main agent: `blocks/team_lead.md` (3.4.0: it alone adds; slots; positions; modes and request + options; replacing a subagent that ran out of context) and the notes of `team.md` §10. +- A subagent: `blocks/team_member.md` (3.4.0: its id and position, `team_whoami`, `team_findings`, + `team_finish`, that its context is limited and it will be told when it fills); a sub-manager, + `blocks/team_submanager.md` in place of the whole lead block it got in 3.2.0. diff --git a/harness/team.md b/harness/team.md index 7b612d3..3c6b04b 100644 --- a/harness/team.md +++ b/harness/team.md @@ -1,6 +1,6 @@ # Teams -Spec 3.2.0; the model a member runs on and when a team is offered, 3.3.0 (`subagents.md`). **Subagents that work together.** A session's agent — the **lead** — may add +Spec 3.2.0; the model a member runs on and when a team is offered, 3.3.0 (`subagents.md`); the main agent, the tree, permissions and a subagent's life, 3.4.0 (`subagents.md` §5–§13). **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 @@ -11,6 +11,16 @@ The tools are `tools/team_add.json`, `team_send.json`, `team_read.json`, `team_s 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`. +> **Spec 3.4.0 supersedes parts of this page** (`subagents.md` §5–§13, *Subagents v2*): the lead is +> **the main agent**, and **only it adds, assigns, replaces and stops** — a member with `lead: true` +> is a **sub-manager** that directs the members placed under it (`team_add`'s `parent`) and does not +> add or stop (§4, §9 below); members have a mode no looser than their parent's and may ask the main +> agent for more (`subagents.md` §7); a member ends with `team_finish` or when its context fills +> (`subagents.md` §9); the room carries assignments, requests and decisions besides messages and +> reports; the tree goes over the link (§11). Everything else here stands as written: the size +> setting and its counting, the room and who receives what, delivery and waking, a manager's +> refusals, capacity and waiting for a slot. "Member" here and "subagent" there are the same thing. + ## 1. Words - **Agent**: the lead or a member. Every agent has a **name**: the lead is `lead`; a member the name @@ -243,6 +253,10 @@ agent not under it (`not_yours`). The answer, `stopped`, names the stopped in tr ## 11. Over the link +*3.4.0: the tree and the room are carried — `acp.md` capability `tree`; what follows is how a device +without it is still shown.* + + 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 diff --git a/harness/tools/team_add.json b/harness/tools/team_add.json index 33cc1b4..fa5e340 100644 --- a/harness/tools/team_add.json +++ b/harness/tools/team_add.json @@ -14,7 +14,7 @@ "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, you included; 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}}", + "description": "Add a subagent and start it at once, in the background, on its part of the work. Only you, the main agent, add subagents. You lead: subagents do the work while you plan, coordinate and review — once you have one, 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`). `role` is its position, 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 subagent needs it, and every subagent is told it.\n- `lead: true` makes it a sub-manager: it directs the subagents you place under it (`parent`), reviews their reports and reports to you; it does not add or stop subagents itself.\n- `mode` is its permission mode: its parent's by default, never looser. With a stricter mode you may give `request` options — kinds of action (read, write, execute) it may ask you for with team_request; you decide with team_decide.\n- The person sets how many agents the team may have, you included; past that this is refused. When the model is already serving as many requests as it can at once, the subagent waits for a slot and starts when one frees.\n- It reports with team_finish (or its final reply). Meanwhile team_send messages it, team_read shows the tree and the room, team_assign gives it new work, team_replace replaces it, team_stop stops it.\n- Agents: {{agents}}", "parameters": { "type": "object", "properties": { @@ -26,7 +26,7 @@ "role": { "type": "string", "minLength": 1, - "description": "One line: what it owns" + "description": "Its position, one line: what it owns" }, "task": { "type": "string", @@ -39,7 +39,7 @@ }, "lead": { "type": "boolean", - "description": "May add members of its own and lead an area (default: false)" + "description": "A sub-manager: directs the subagents placed under it (default: false)" }, "agent": { "type": "string", @@ -48,6 +48,32 @@ "model": { "type": "string", "description": "Run it on another model — only one the person named in this conversation (its id or name, as in the list in your instructions). Default: the session's model." + }, + "parent": { + "type": "string", + "description": "The sub-manager to place it under (default: you, lead)" + }, + "mode": { + "type": "string", + "enum": [ + "auto", + "edit", + "manual", + "plan" + ], + "description": "Its permission mode: its parent's or stricter (default: its parent's)" + }, + "request": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "read", + "write", + "execute" + ] + }, + "description": "With a stricter mode only: kinds of action it may ask you for (team_request)" } }, "required": [ @@ -58,5 +84,6 @@ }, "variables": { "agents": "the agents a member may be: `name — description`, separated by \"; \" (as task's)" - } + }, + "offered": "main" } diff --git a/harness/tools/team_assign.json b/harness/tools/team_assign.json new file mode 100644 index 0000000..24ec0f6 --- /dev/null +++ b/harness/tools/team_assign.json @@ -0,0 +1,51 @@ +{ + "name": "team_assign", + "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": [] + }, + "offered": "main", + "description": "Give a subagent that has not ended a new task: it is told at its next step, or woken. Optionally a new position (`role`), another sub-manager to work under (`parent`, or lead), or a stricter `mode`.", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "task": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string" + }, + "parent": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "auto", + "edit", + "manual", + "plan" + ] + } + }, + "required": [ + "name", + "task" + ] + } +} diff --git a/harness/tools/team_decide.json b/harness/tools/team_decide.json new file mode 100644 index 0000000..d31bca2 --- /dev/null +++ b/harness/tools/team_decide.json @@ -0,0 +1,38 @@ +{ + "name": "team_decide", + "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": [] + }, + "offered": "main", + "description": "Decide a subagent's request (team_request): allow it or refuse it, with a note it is told. You may allow only what your own mode would allow you; anything beyond that goes to the person when the subagent tries.", + "parameters": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "allow": { + "type": "boolean" + }, + "note": { + "type": "string" + } + }, + "required": [ + "id", + "allow" + ] + } +} diff --git a/harness/tools/team_findings.json b/harness/tools/team_findings.json new file mode 100644 index 0000000..a6b85d4 --- /dev/null +++ b/harness/tools/team_findings.json @@ -0,0 +1,41 @@ +{ + "name": "team_findings", + "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": "Findings kept beyond your context: `write` adds one (a fact, a result, where something is) to yours; `read` shows yours, or with `of` those of a subagent under you (the main agent: anyone's), ended ones included. Write as you go: when your context fills you have one turn left, and your findings are what the next agent starts from.", + "parameters": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "write", + "read" + ] + }, + "text": { + "type": "string", + "description": "write: the finding" + }, + "of": { + "type": "string", + "description": "read: a name or an id (default: yours)" + } + }, + "required": [ + "action" + ] + } +} diff --git a/harness/tools/team_finish.json b/harness/tools/team_finish.json new file mode 100644 index 0000000..3b7d54a --- /dev/null +++ b/harness/tools/team_finish.json @@ -0,0 +1,31 @@ +{ + "name": "team_finish", + "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": [] + }, + "offered": "subagent", + "description": "Your task is done: send your report to your parent and end. Make the report complete on its own — the result, then the evidence (file:line), then anything you could not settle. Save anything worth keeping beyond the report with team_findings first. Nothing runs after this call.", + "parameters": { + "type": "object", + "properties": { + "report": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "report" + ] + } +} diff --git a/harness/tools/team_read.json b/harness/tools/team_read.json index c59c116..fae76ae 100644 --- a/harness/tools/team_read.json +++ b/harness/tools/team_read.json @@ -14,7 +14,7 @@ "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.", + "description": "The tree and its room: every agent (name, id, position, parent, state, model, mode, context use, last activity), open slots, subagents that have ended, and the room's messages after `since` that are yours to see — those you sent or received and those of the subagents under you; the main agent sees them all. Messages are numbered: pass the last number you saw as `since` to read only what is new.", "parameters": { "type": "object", "properties": { diff --git a/harness/tools/team_replace.json b/harness/tools/team_replace.json new file mode 100644 index 0000000..58f5402 --- /dev/null +++ b/harness/tools/team_replace.json @@ -0,0 +1,34 @@ +{ + "name": "team_replace", + "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": [] + }, + "offered": "main", + "description": "Replace a subagent — one that ran out of context, or one that is stuck: it ends (stopped, if it has not already), and a new one with the same name, position, parent, mode and model takes its place, briefed with `task` (default: the old task) and the old one's findings. The subagents under it move under the new one.", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "task": { + "type": "string" + } + }, + "required": [ + "name" + ] + } +} diff --git a/harness/tools/team_request.json b/harness/tools/team_request.json new file mode 100644 index 0000000..e4feb82 --- /dev/null +++ b/harness/tools/team_request.json @@ -0,0 +1,41 @@ +{ + "name": "team_request", + "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": [] + }, + "offered": "subagent", + "description": "Ask the main agent to allow a call your mode does not, of a kind your request options name. Say the tool, the permission pattern you need (a command such as `git push *`, or a path) and why. Its decision comes to you as a message; once allowed, make the call again.", + "parameters": { + "type": "object", + "properties": { + "tool": { + "type": "string", + "minLength": 1 + }, + "pattern": { + "type": "string", + "minLength": 1 + }, + "why": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "tool", + "pattern", + "why" + ] + } +} diff --git a/harness/tools/team_send.json b/harness/tools/team_send.json index 88da764..7e6fea1 100644 --- a/harness/tools/team_send.json +++ b/harness/tools/team_send.json @@ -14,7 +14,7 @@ "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.", + "description": "Send a message through the team's room: to one or more agents by name, to `parent` (the agent you report to), 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 main agent is `lead`.", "parameters": { "type": "object", "properties": { @@ -24,7 +24,7 @@ "type": "string" }, "minItems": 1, - "description": "Names, or [\"all\"]" + "description": "Names, [\"parent\"], or [\"all\"]" }, "message": { "type": "string", diff --git a/harness/tools/team_stop.json b/harness/tools/team_stop.json index 94b5d94..bddf82e 100644 --- a/harness/tools/team_stop.json +++ b/harness/tools/team_stop.json @@ -14,18 +14,19 @@ "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.", + "description": "Stop a subagent, and the subagents under it, where they are: their work stops and they end, which makes room for others. Only you, the main agent, stop subagents; `all` stops every one. Nothing they had not reported comes back — ask for a report with team_send first, or read their findings with team_findings.", "parameters": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, - "description": "A member under you, or all" + "description": "A subagent's name, or all" } }, "required": [ "name" ] - } + }, + "offered": "main" } diff --git a/harness/tools/team_whoami.json b/harness/tools/team_whoami.json new file mode 100644 index 0000000..c4df52c --- /dev/null +++ b/harness/tools/team_whoami.json @@ -0,0 +1,22 @@ +{ + "name": "team_whoami", + "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": "Who you are on the team: your id, name, position, parent, task, permission mode and request options, model, limits, and how full your context is.", + "parameters": { + "type": "object", + "properties": {} + } +}