Compare commits
65
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7627c342d6
|
||
|
|
38a52d2e7f
|
||
|
|
6da9c7fcd0
|
||
|
|
7f7cd148aa
|
||
|
|
cb7800cc6e
|
||
|
|
a3e9a43dd6
|
||
|
|
a4027d5626
|
||
|
|
d111cad22e
|
||
|
|
788db0ff4d
|
||
|
|
e349d829a1
|
||
|
|
fe0fa62a51
|
||
|
|
f3db970c9d
|
||
|
|
ae1201540a
|
||
|
|
b70cfe483b
|
||
|
|
ecfd1b0b8a
|
||
|
|
8c5edc0f01
|
||
|
|
b66891e186
|
||
|
|
6199028358
|
||
|
|
4b8786b7f6
|
||
|
|
6a0e0ed145
|
||
|
|
3a206959b6
|
||
|
|
6993937522
|
||
|
|
f88921667e
|
||
|
|
0c3596d044
|
||
|
|
6e0582dc22
|
||
|
|
a26dd8a5e0
|
||
|
|
02c7282e74
|
||
|
|
9499e93bc4
|
||
|
|
6a82e6cb61
|
||
|
|
536fd2aa45
|
||
|
|
d3af8c2f02
|
||
|
|
6ec95fbd66
|
||
|
|
fbefe7f844
|
||
|
|
5c23087cef
|
||
|
|
880289970b
|
||
|
|
d09d51cfec
|
||
|
|
fdfcf3666a
|
||
|
|
a97c5b46a4
|
||
|
|
aa83bb9783
|
||
|
|
8e06590d57
|
||
|
|
ecae6247fd
|
||
|
|
0d04c5300f
|
||
|
|
a029ee6f07
|
||
|
|
059d34cec1
|
||
|
|
559b521ca9
|
||
|
|
ef9dcf33c9
|
||
|
|
b1dcaded72
|
||
|
|
4ca6af12b1
|
||
|
|
eacf84fd67
|
||
|
|
89ceeec159
|
||
|
|
77161bee35
|
||
|
|
ef209f41a1
|
||
|
|
d5043ab0db
|
||
|
|
a73adc01f0
|
||
|
|
6d5a25cdad
|
||
|
|
28ff205ed0
|
||
|
|
f20e8c4307
|
||
|
|
8cfbfbbca4
|
||
|
|
e7300b26d5
|
||
|
|
4bbee18cee
|
||
|
|
bc78aec424
|
||
|
|
9f536d8345
|
||
|
|
6ca43b6908
|
||
|
|
f606c5d1e7
|
||
|
|
253ef4a8a0
|
No files matched your search
+273
-1
@@ -4,7 +4,279 @@ All notable changes to LLeMbas CLI. The format follows
|
|||||||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
||||||
[Semantic Versioning](https://semver.org/).
|
[Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
## [Unreleased]
|
## [2.0.0-beta.8] — 2026-11-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Harness spec 3.6.0: phone actions.** The spec now describes `phone_action`, the LLeMbas
|
||||||
|
server's tool for doing something on the person's phone when they write from the LLeMbas app on
|
||||||
|
it — alarms and timers, calendar events, SMS and email drafts, the dialer, routes (start one,
|
||||||
|
add a stop, search nearby, stop), opening an app or an address, media, the torch, settings
|
||||||
|
panels and automations — confirmed on the phone every time (`harness/phone.md`). The server's
|
||||||
|
alone: the CLI does not offer it, and its conformance area is the server's.
|
||||||
|
|
||||||
|
## [2.0.0-beta.7] — 2026-11-10
|
||||||
|
|
||||||
|
Harness spec 3.5.0: scheduled work on a linked computer.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Accept scheduled work.** A new switch, off by default: `lembas service scheduled on` (or
|
||||||
|
`lembas remote scheduled on`; `remote.scheduled` in the global config). On, the schedules you
|
||||||
|
make on your LLeMbas server may run on this computer as agent turns, in the folders remote work
|
||||||
|
may use. `lembas remote list --json` says `scheduled`; the server is told at once (the link
|
||||||
|
dials again once nothing is working).
|
||||||
|
- **Scheduled turns.** A run is an agent session in the schedule's mode — Edit unless the schedule
|
||||||
|
says Auto, never Plan, and never beyond `remote.max_mode` — with the schedule's Subagents
|
||||||
|
Min/Max. Its answer reports the run: `ok`, `silent` (the reply said `[SILENT]` on its first or
|
||||||
|
last line), `failed` or `cancelled`, and the final reply. A switch turned off is a fixed refusal
|
||||||
|
the server shows as it is.
|
||||||
|
- **Approvals in a scheduled run** go to you through the server (a notification on your phone)
|
||||||
|
and wait the schedule's wait — 30 minutes unless it says otherwise — then count as denied, and
|
||||||
|
the model is told so. Questions and plans have nobody to answer in a scheduled run.
|
||||||
|
- **Watch commands** (monitor mode): the server asks this computer to run a schedule's watch
|
||||||
|
command in its folder, with a timeout, and gets back a hash of the output and a short excerpt;
|
||||||
|
the model runs only when the output changed. A command the schedule's mode would ask about is
|
||||||
|
refused rather than run.
|
||||||
|
|
||||||
|
- **`/schedule`**: your schedules on the LLeMbas server from the terminal — the list with each
|
||||||
|
one's next run and last result, `new <words>` (the server works out the rule; you see what it
|
||||||
|
understood and the next runs before anything is made, and may have it run on this computer in
|
||||||
|
the current folder), `pause`, `resume`, `run` (now, without using up the next scheduled run) and
|
||||||
|
`history`. It uses this computer's sign-in; a server too old to allow that says so.
|
||||||
|
|
||||||
|
- **A notice when a reply finishes** — or when the agent waits for your answer — while the
|
||||||
|
terminal is not focused: the bell, and a desktop notification (OSC 9, in terminals that show
|
||||||
|
one) naming the session, never what the reply says. `notify: false` (or `/settings`) turns it
|
||||||
|
off.
|
||||||
|
- **`/rename <title>`** names this session — in `/sessions`, and in the web UI when it is shared.
|
||||||
|
- **`/search <words>`** finds a past session in this project by what was said in it (add `all`
|
||||||
|
for every project) and resumes the one you pick.
|
||||||
|
- **`/memory` shows the server's memory** when the server keeps it (`library: lembas`): its
|
||||||
|
entries beside this machine's files, each one to change or forget, through the server.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `team_add` under a sub-manager says where the report goes: "Its report goes to <sub-manager>
|
||||||
|
when it finishes", not "comes to you".
|
||||||
|
|
||||||
|
## [2.0.0-beta.6] — 2026-11-10
|
||||||
|
|
||||||
|
Harness spec 3.4.0 (spec commits 4b8786b, b66891e): one progress vocabulary for every client, and
|
||||||
|
subagents organised as the main agent's tree.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Progress phases.** The line under the conversation says what the agent is doing in the words
|
||||||
|
every LLeMbas client uses — *Queued*, *Waiting for {model}*, *Reading the prompt: n / total
|
||||||
|
tokens (p%)*, *Thinking*, *Writing: n tok/s*, *Running {tool}: {what}*, *Waiting for you*,
|
||||||
|
*Waiting for subagents*, *Compacting the conversation*, *Retrying in n s*, *Stopped*, *Failed* —
|
||||||
|
with how long it has lasted, then the turn's time and tokens. The CLI keeps its own estimate of
|
||||||
|
how long the rest of a long prompt will take. Over the link each phase is a `phase` event and is
|
||||||
|
in the session's status (capability `progress`).
|
||||||
|
- **The main agent and its subagents.** Only the main agent adds, reassigns (`team_assign`),
|
||||||
|
replaces (`team_replace`), stops and decides; a subagent added with `lead: true` is a
|
||||||
|
sub-manager that directs the subagents placed under it (`parent`) and adds none. Each subagent
|
||||||
|
has an id (`s1`, `s2`, …), a name, a position, a parent, a task, a mode and a model, and can ask
|
||||||
|
`team_whoami`. With Min above 1 the main agent starts with Min − 1 open slots to fill.
|
||||||
|
- **Permissions only go down.** A subagent's mode is its parent's or stricter. A narrowed one may
|
||||||
|
be given request options (read, write, execute) and ask the main agent with `team_request`; the
|
||||||
|
main agent decides with `team_decide`, and never allows more than its own mode would. What
|
||||||
|
nobody in the tree could allow — a `bash` call in an all-Edit tree — is put to you, on an
|
||||||
|
approval card that names the subagent.
|
||||||
|
- **A subagent's end.** It reports and ends with `team_finish`. Its findings (`team_findings`) are
|
||||||
|
kept after it ends. When its context fills it does not compact: it is paused, gets one last
|
||||||
|
turn of at most 8 steps to save its findings and tell its parent, and ends; the main agent
|
||||||
|
replaces it or uses what it found.
|
||||||
|
- **Seeing the tree.** `/team` draws every agent with its position, state, model, mode, context
|
||||||
|
use, current phase and messages, the open slots and the subagents that ended; `/team open
|
||||||
|
<name>` shows a subagent's session read-only, live or after it ended; `/team findings <name>`;
|
||||||
|
`/context <name>` for a subagent's context.
|
||||||
|
- **An agent chat's subagents run on this device.** The chat's Min and Max come with every turn
|
||||||
|
(capability `agents`) and the tree, progress, findings and transcripts go back to the server
|
||||||
|
(capability `tree`: the `team` event with the tree, `_lembas/team`, `/transcript`, `/findings`,
|
||||||
|
`/stop`).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- A subagent can no longer add subagents of its own or start a `task`; it asks the main agent.
|
||||||
|
`team_stop` is the main agent's alone.
|
||||||
|
|
||||||
|
## [2.0.0-beta.5] — 2026-11-09
|
||||||
|
|
||||||
|
Harness spec 3.3.0 (spec commit 02c7282): subagents run on your model unless you name another,
|
||||||
|
and `/context` shows where the window goes the way the side panel of the other apps does.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Subagents run on the session's model**, unless you named another model in this session's own
|
||||||
|
messages — its id, the part after its provider, or its display name, as a whole word, in any
|
||||||
|
letter case. A `task` or `team_add` that asks for any other model is refused ("Use another
|
||||||
|
model for subagents only when the person names it."), and an agent file's own `model:` is no
|
||||||
|
longer used. Nothing in the settings chooses a subagent's model.
|
||||||
|
- **Only the logical capacity limits remain.** A named model on the same one-model server as the
|
||||||
|
session's model is refused ("That model is on the same one-model server as this chat's model;
|
||||||
|
name a model on another server."). On a model that serves one request at a time, a subagent on
|
||||||
|
that same model is refused ("This model serves one request at a time: name another model, on
|
||||||
|
another server, for subagents.") and nothing else is picked instead.
|
||||||
|
- **Subagents are offered** — the size setting, the team tools — when subagents are on and some
|
||||||
|
model could serve one: the session's, when it serves two requests at once, or any model of the
|
||||||
|
server that is not on its one-model server. When the session's model cannot serve its own
|
||||||
|
subagents, the agent is told so in its first note, and `/effort`, `/team size` and `/team` say it
|
||||||
|
beside the setting.
|
||||||
|
- **"Subagents" is the word** for the setting, with **Min** and **Max**: the row beside the effort
|
||||||
|
in `/effort`, `/team size`, `/settings`. "Helper" is gone. The status bar reads
|
||||||
|
`agents 3/4 · 2 subagents working` while there is a team.
|
||||||
|
- **The numbers count this agent**: `Subagents: Min 1, Max 4 — 1 is this agent alone`. Max 1 is
|
||||||
|
the agent alone, with no subagents; Max 4 is the agent and up to three subagents.
|
||||||
|
- **The setting is one range with two handles.** In `/effort` and `/team size` the Subagents row
|
||||||
|
shows `Min 1`, a track from 1 to the cap with a handle for each, and `Max 4`: ←→ move the handle in focus, tab switches to the other, enter
|
||||||
|
sets both for the session (Min never passes Max; Max goes up to `agents.cap`). `/team size` also
|
||||||
|
keeps the usual pairs and `<min> <max>` typed.
|
||||||
|
- **`/context`** shows the window as used tokens of the window and a percentage, one bar split by
|
||||||
|
category, every category with its tokens and its share of the window, what is free, and how far
|
||||||
|
it is to automatic compaction (`N until auto-compact`, with `/compact` to do it now).
|
||||||
|
|
||||||
|
## [2.0.0-beta.4] — 2026-11-09
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`lembas login` no longer loops on a sign-in the server stopped accepting.** With an earlier login
|
||||||
|
for the same server kept here (from an older install, or LLeMbas Desktop), login only refreshed it;
|
||||||
|
when the server refused the old token it said “run login again”, and running it again did the same.
|
||||||
|
Now a refused sign-in starts a new one, which replaces the old.
|
||||||
|
- The installer's last line says to run `lembas login`, not to edit `connections.yaml` (gone in 2.0).
|
||||||
|
|
||||||
|
## [2.0.0-beta.3] — 2026-11-09
|
||||||
|
|
||||||
|
Harness spec 3.2.0: teams, effort levels that mean what they say, the model's reasoning sent
|
||||||
|
back while it works, and no absolute paths in the prompt.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Teams.** Where the model serves at least two requests at once, the agent can add **members**
|
||||||
|
(`team_add`): background agents that each own a part of the work, briefed on their own and told
|
||||||
|
the team's goal. Once it has a member the agent **leads**: tools that change things answer that
|
||||||
|
it should give the work to a member, while reading, reviewing and managing stay. Members and the
|
||||||
|
lead talk in the team's **room** (`team_send`, to a name or to `all`); a member's final reply
|
||||||
|
comes back as its report and wakes the lead if it was idle, and a message wakes a member that
|
||||||
|
had finished. A member made with `lead: true` leads an area with members of its own.
|
||||||
|
`team_read` shows the team and the room, `team_stop` stops a member and everything under it.
|
||||||
|
- **How many agents a team may have**: the `agents` setting (`min` 1 and `max` 4 by default, at
|
||||||
|
most `agents.cap`, 32), changed with `/team size <min> <max>`, from the **agents** row beside the
|
||||||
|
effort in `/effort`, or in `/settings`; the agent is told the size and every change in a note.
|
||||||
|
`max: 1` means one agent and no helpers; `min: 2` or more makes the agent lead from the start.
|
||||||
|
`subagents: false` switches teams off together with `task`.
|
||||||
|
- **`/team`** shows the team as a tree and the room, and stops a member (`/team stop <name>`); the
|
||||||
|
agent is told who you stopped. The status bar shows `team 3/4 · 2 working` while there is a team.
|
||||||
|
- When every slot of the model is busy, a member (or the agent itself) **waits for a free slot**
|
||||||
|
and starts when one frees, instead of being refused; one-off `task` helpers count toward the
|
||||||
|
team's size.
|
||||||
|
- **A Thinking switch.** A model that can switch thinking off offers `off` as a real level: no
|
||||||
|
thinking at all, not the model's default. `/effort` shows a list for several levels, an on/off
|
||||||
|
switch where that is the only choice, and nothing where there is nothing to choose.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **The model's reasoning goes back to it during its turn**, so it carries on from what it thought
|
||||||
|
instead of working it out again at every step; earlier turns are sent without it, as before.
|
||||||
|
- **No absolute paths in the prompt.** The agent is told the project's name and, when you started
|
||||||
|
below its root, that directory relative to it. Relative paths in every file tool and the
|
||||||
|
directory `bash` starts in are taken from the project root; absolute paths still work.
|
||||||
|
Instruction files are named from the project root or as `~/…`.
|
||||||
|
|
||||||
|
## [2.0.0-beta.2] — 2026-11-07
|
||||||
|
|
||||||
|
Harness spec 3.1.0: what the model has already read stays exactly as it was, so its server
|
||||||
|
can reuse it instead of reading the whole session again.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **The system prompt is made once per session** — at the start, after a compaction, on a model
|
||||||
|
switch — kept with the session and sent byte for byte on every request; a resumed session sends
|
||||||
|
the one it kept. It holds no date any more.
|
||||||
|
- **What changes during a session is said in a note** after your words on your next message: the
|
||||||
|
date (on the first message, and when it changes), a permission mode you chose, a plan you
|
||||||
|
changed, memory or an instruction file you edited, a personality you picked, an MCP server's
|
||||||
|
instructions that arrived later. The model's own changes (its plan, its edits, a plan approval)
|
||||||
|
are not said again. Notes are not shown as your words.
|
||||||
|
- **The tool list is the same for the whole session.** `plan_submit` is offered in every mode and
|
||||||
|
answers that it works only in plan mode; `show` is offered from the start where the session can
|
||||||
|
have a viewer; knowledge search is decided as the session starts. The tools stay listed when a
|
||||||
|
step limit or budget winds a prompt up (a call in that answer is not run), and a tool refused
|
||||||
|
for good stays listed and answers that refusal. The first request waits up to 3 seconds for the
|
||||||
|
configured MCP servers, so their tools are in the first list.
|
||||||
|
- **Compaction asks for its summary as the next turn of the same conversation** instead of a
|
||||||
|
separate request with a pasted transcript, so it reuses what the server has read and no longer
|
||||||
|
loses the middle of a long session. A tool call in the summary's reply is not run.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Cached input tokens are recorded for every request** (the usage's own count, or llama.cpp's
|
||||||
|
`timings` and progress), kept with the session, and shown by `/context` for the last request.
|
||||||
|
|
||||||
|
## [2.0.0-beta.1] — 2026-11-03
|
||||||
|
|
||||||
|
LLeMbas CLI 2.0 works with LLeMbas 2.0 and nothing else: it uses the server it is logged in to,
|
||||||
|
keeps tool results short without ever rewriting what the model has already read, and reads every
|
||||||
|
instruction file on the way down to where it was started. Harness spec 3.0.0.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **The CLI talks only to the LLeMbas server it is logged in to.** Custom connections are gone:
|
||||||
|
the OpenAI-compatible, Responses, Anthropic, Gemini and Ollama connections in `connections.yaml`,
|
||||||
|
with their `dialect`, `key_cmd`, `auth`, `discover`, `unload_url` and `one_model_at_a_time`,
|
||||||
|
and a model's `fallback`. Log in with `lembas login <address>`; without a login the CLI says so
|
||||||
|
and how. An old `connections.yaml` still starts: an entry that is not the login is named in a
|
||||||
|
warning and ignored. One server at a time: logging in to another signs this machine out of the
|
||||||
|
first.
|
||||||
|
- **Models are named as the server serves them** (`deepseek/deepseek-flash`), with no connection
|
||||||
|
in front; a `config.yaml` naming one the old way is rewritten once. The context window,
|
||||||
|
efforts and capacity are the server's word.
|
||||||
|
- **Memory is the server's**, shared with your chats there (when the login's token may reach it):
|
||||||
|
the `memory` tool and the memory in the prompt. Notes, knowledge bases and skills stay on this
|
||||||
|
machine.
|
||||||
|
- **Logging in asks for the `memory` scope** in place of `library`; a login made before keeps
|
||||||
|
working with the one it has.
|
||||||
|
- **Usage is money, not credits.** The status bar shows what the month has cost so far, and
|
||||||
|
`/usage` the month's tokens and cost and this device's tokens — each currency's amount on its
|
||||||
|
own (`€2.00 · $0.75`), never added together. Plans, balances, rolling windows and the
|
||||||
|
`unlimited` of an administrator are gone with the server's credits.
|
||||||
|
- **Instruction files stack.** Every `AGENTS.md` (or `CLAUDE.md`) from the project root down to the
|
||||||
|
working directory goes to the model, outermost first — not only the nearest one.
|
||||||
|
- **Tool results are kept short when they come in, and never changed afterwards**, so the server's
|
||||||
|
prompt cache holds. Every result has an id (`[#0001]`); a read says its lines and a hash of the
|
||||||
|
file; reading an unchanged file again, or the same listing or search with the same result,
|
||||||
|
answers with a pointer to the earlier result; output over 400 lines or 40,000 characters keeps its
|
||||||
|
beginning and its end; edits answer with their diff. `compaction.prune`, which cut old outputs
|
||||||
|
and made the server read the whole conversation again, is removed.
|
||||||
|
- **The link speaks protocol 3** with a LLeMbas 2.0 server. A 1.x server is refused at login and by
|
||||||
|
the link, saying to update the server (or use LLeMbas CLI 1.x); a newer one says to update the CLI.
|
||||||
|
- **A chat started on a model runs on that model.** When the web UI opens a session on this device
|
||||||
|
with a model the device cannot run, it is refused with the reason instead of being answered on
|
||||||
|
the device's own model.
|
||||||
|
- **File tools speak the server's words.** A missing file is `No such file or directory: <path>`
|
||||||
|
from `read`, `edit`, `multiedit`, `show`, `list`, `glob` and `grep` alike; a file `write` or
|
||||||
|
`edit` may not change yet says it has not been read, or changed since it was last read, with its
|
||||||
|
full path. `glob` and `grep` give full paths. `list` sorts names exactly (capitals first) and says
|
||||||
|
when it stopped at 400 lines.
|
||||||
|
- **A new skill's name** is lowercase letters, digits and `-`, up to 64 characters, as the Agent
|
||||||
|
Skills format has it; `_` and `.` are no longer accepted when `skill_manage` makes one. Skills
|
||||||
|
that already have such a name still load.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`recall`**: the model can see any earlier tool result again, in full or from a line on — the
|
||||||
|
part of a long output left out, or a result from before the conversation was summarised. After
|
||||||
|
a summary the model also gets a table of the files it had read.
|
||||||
|
- **`show`**, in a session the web UI or LLeMbas Desktop opened: the model can open a file it made
|
||||||
|
— a page, a document — in your viewer beside the chat.
|
||||||
|
- **`lembas login --token-stdin --server <url>`**: sign in with a token a program already holds —
|
||||||
|
LLeMbas Desktop, from the one approval that gives it the agent's token beside its own. The token
|
||||||
|
is read from stdin only, never from an argument or the environment, and checked against the
|
||||||
|
server first: it must speak protocol 3, accept the token, and say it carries `link`. The login
|
||||||
|
is then stored exactly as a sign-in by code (an earlier one replaced and its token signed out),
|
||||||
|
and `--json` prints the same `done` (or `error`) line.
|
||||||
|
|
||||||
## [1.5.2] — 2026-10-25
|
## [1.5.2] — 2026-10-25
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,11 @@
|
|||||||
|
|
||||||
# LLeMbas CLI
|
# LLeMbas CLI
|
||||||
|
|
||||||
A terminal coding agent and project manager: the `lembas` command. It talks to any LLM API
|
A terminal coding agent and project manager: the `lembas` command. It signs in to a
|
||||||
(OpenAI-compatible servers such as llama.cpp, vLLM or LM Studio, the OpenAI Responses API,
|
[LLeMbas](https://github.com/LLeMbas/LLeMbas) server and uses its models, memory and search, works
|
||||||
Anthropic, Gemini, Ollama), works in your project with four permission modes and a git snapshot of
|
in your project with four permission modes and a git snapshot of every turn, keeps its own notes,
|
||||||
every turn, remembers what it learns across sessions, and can sign in to a
|
knowledge bases and skills on your machine, and lets the server's web UI work the same sessions.
|
||||||
[LLeMbas](https://github.com/LLeMbas/LLeMbas) server to use its models, library and search — and
|
Linux, x64 and arm64.
|
||||||
let its web UI work the same sessions. Linux, x64 and arm64.
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@@ -24,12 +23,16 @@ checksum before installing anything ([SECURITY.md](SECURITY.md) shows how to che
|
|||||||
## First steps
|
## First steps
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd your-project && lembas # the TUI
|
lembas login https://your-server # sign in to your LLeMbas server
|
||||||
lembas login https://your-server # sign in to a LLeMbas server and use its models
|
cd your-project && lembas # the TUI, on the server's models
|
||||||
```
|
```
|
||||||
|
|
||||||
Without a server, add a connection to `~/.config/lembas/connections.yaml` and set `model:` in
|
A program that already holds an agent's token for this machine (LLeMbas Desktop does, from the
|
||||||
`~/.config/lembas/config.yaml`.
|
same approval as its own) hands it over on stdin, never as an argument:
|
||||||
|
`… | lembas login --token-stdin --server https://your-server [--json]`.
|
||||||
|
|
||||||
|
From 2.0 the CLI talks only to the LLeMbas server it is logged in to; the custom connections of 1.x
|
||||||
|
(`dialect:` entries in `connections.yaml`) are gone.
|
||||||
|
|
||||||
`lembas` updates itself (`lembas update` does it now); `lembas uninstall` removes it, and
|
`lembas` updates itself (`lembas update` does it now); `lembas uninstall` removes it, and
|
||||||
`--purge` takes settings and sessions too.
|
`--purge` takes settings and sessions too.
|
||||||
|
|||||||
+248
-9
@@ -7,7 +7,7 @@ metadata. This directory writes that design down as data. Each project implement
|
|||||||
language; neither ever runs the other. LLeMbas CLI embeds these files at build time; LLeMbas vendors a pinned, hash-checked copy.
|
language; neither ever runs the other. LLeMbas CLI embeds these files at build time; LLeMbas vendors a pinned, hash-checked copy.
|
||||||
|
|
||||||
The concept-by-concept map of the two implementations, and where they still differ, is the wiki
|
The concept-by-concept map of the two implementations, and where they still differ, is the wiki
|
||||||
page [Harness-parity](https://git.houmeres.sk/LLeMbas/LLeMbas-CLI/wiki/Harness-parity).
|
page *Harness-parity* in LLeMbas CLI's wiki.
|
||||||
|
|
||||||
## What is here
|
## What is here
|
||||||
|
|
||||||
@@ -15,14 +15,20 @@ page [Harness-parity](https://git.houmeres.sk/LLeMbas/LLeMbas-CLI/wiki/Harness-p
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `VERSION` | The spec's own version, apart from LLeMbas CLI's |
|
| `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 |
|
| `purpose.json` | The `purpose` argument added to a tool whose calls say what they are for |
|
||||||
| `tools/<name>.json` | One tool each: name, `scope`, `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`), the description and the JSON Schema of its parameters — exactly what a model is sent |
|
| `tools/<name>.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 |
|
| `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/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/arity.json` | How much of a command an "always allow" covers |
|
||||||
| `permission/defaults.json` | The rules every session starts from |
|
| `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 |
|
| `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, vision, tools, notes, capacity |
|
| `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 |
|
| `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; `scheduled` (3.5.0) — scheduled work on a linked computer: the setting, the modes, the approval wait, the sentinel, the watch command's limits and the refusals; `phone` (3.6.0) — phone actions: the capability, each action's fields, what is required and checked, the wait and the texts |
|
||||||
|
| `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; 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 |
|
||||||
|
| `phone.md` | Phone actions (3.6.0): `phone_action`, carried out by the LLeMbas app on the person's phone and confirmed there — who offers it, how a call is checked, the answer, navigation |
|
||||||
|
| `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 |
|
| `quirks.json` | What real servers do, and how both harnesses cope |
|
||||||
| `conformance/<area>.json` | Cases with their expected results, run by both test suites |
|
| `conformance/<area>.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 |
|
| `acp.md` | The link between them: ACP plus every `_lembas/*` method, field and capability |
|
||||||
@@ -30,22 +36,253 @@ page [Harness-parity](https://git.houmeres.sk/LLeMbas/LLeMbas-CLI/wiki/Harness-p
|
|||||||
## Vocabulary
|
## Vocabulary
|
||||||
|
|
||||||
- **Risk classes:** `read`, `write`, `execute`, `interact`. **Reading and writing are never one
|
- **Risk classes:** `read`, `write`, `execute`, `interact`. **Reading and writing are never one
|
||||||
tool** among the shared ones, so a read-only helper is given exactly the readers.
|
tool** among the shared ones, so a read-only subagent is given exactly the readers.
|
||||||
- **Modes:** `manual`, `edit`, `auto`, `plan` (`unrestricted` is read as `auto`).
|
- **Modes:** `manual`, `edit`, `auto`, `plan` (`unrestricted` is read as `auto`).
|
||||||
- **Scopes:** `shared` — both projects implement it; `cli` or `llembas` — only that one. The same
|
- **Scopes:** `shared` — both projects implement it; `cli` or `llembas` — only that one. The same
|
||||||
two names key what is per project elsewhere: a tool's `aliases`, `loop.json`'s budgets, a
|
two names key what is per project elsewhere: a tool's `aliases`, `loop.json`'s budgets, a
|
||||||
protected path's `scope`.
|
protected path's `scope`.
|
||||||
|
- **Sessions** (a tool's `sessions`, spec 3.0.0): where a tool is offered — `chat`, a conversation
|
||||||
|
on the server, working in the server's file system (`filesystem.md`); `agent`, a session on a
|
||||||
|
device (the CLI's own, Desktop's agent — which is the CLI — or an agent chat the web UI runs on a
|
||||||
|
linked device), working on that machine's disk; `both`. The two say different things: `read` is
|
||||||
|
`shared` and `both` (each project implements it for its own world); `bash` is `cli` and `agent`;
|
||||||
|
`mkdir` is `llembas` and `chat`.
|
||||||
|
- **Requires** (spec 3.0.0): what must hold for a tool to be offered at all — `page`: a browser
|
||||||
|
page of the chat is open and attached (not a schedule, not a background job); `display`: the
|
||||||
|
session is shown somewhere a file can be opened (the web UI, Desktop; not a bare terminal);
|
||||||
|
`forge`: the session's view holds a repository mounted from a forge; `plan`: the session is in
|
||||||
|
plan mode; `team` (3.2.0): the session's team setting allows more than one agent, and a model could
|
||||||
|
serve as a subagent (3.3.0: `subagents.md` §4; before, the chat's model serving two requests at
|
||||||
|
once); `phone` (3.6.0): a message in the chat came from an app on the person's phone that
|
||||||
|
carries out phone actions (`phone.md` §1). A requirement that goes away between the offer and the call makes the call answer the
|
||||||
|
tool's `unavailable` text, word for word, as its result. **From 3.1.0 a requirement that can come
|
||||||
|
and go during a session does not take the tool out of the list** (`context.md` §10, rule 4): the
|
||||||
|
tool is offered from the start and answers `unavailable` while the requirement is not met.
|
||||||
|
- **Confirm** (spec 3.0.0): `always` — the call is put to the person before it runs, in every mode,
|
||||||
|
auto included, and runs on a yes only; nothing remembers the answer (`permission/modes.json`).
|
||||||
- A tool's other names, per project, are listed in its `aliases` and work as the tool. A former
|
- A tool's other names, per project, are listed in its `aliases` and work as the tool. A former
|
||||||
name may have taken different arguments; the project converts them. The one such case:
|
name may have taken different arguments; the project converts them. The one such case:
|
||||||
**`todo` → `plan_write`**, `todos[]` becoming one phase called "Work" (`pending` → `todo`,
|
**`todo` → `plan_write`**, `todos[]` becoming one phase called "Work" (`pending` → `todo`,
|
||||||
`in_progress` → `doing`, `completed` → `done`, `cancelled` → `dropped`; `conformance/plan.json`).
|
`in_progress` → `doing`, `completed` → `done`, `cancelled` → `dropped`; `conformance/plan.json`).
|
||||||
- **The plan** (`plan_write`, `plan_update`, `plan_submit`) is one structure both keep per session —
|
- **The plan** (`plan_write`, `plan_update`, `plan_submit`) is one structure both keep per session —
|
||||||
title, summary, findings, objectives, phases of tasks with a status each, ids given by the
|
title, summary, findings, objectives, phases of tasks with a status each, ids given by the
|
||||||
implementation and never by the model — shown as a card in every UI and put into the prompt
|
implementation and never by the model — shown as a card in every UI. From 3.1.0 it is in the
|
||||||
(`{{plan_context}}`) on every step while it exists. `plan_write` and `plan_update` are offered in
|
system prompt (`{{plan_context}}`) only as it was when the prompt was made; the model sees it change
|
||||||
every mode; `plan_submit` only in plan mode, where it is the approval. Their permission key is
|
in its own `plan_write` / `plan_update` results, and a change by the person comes as a note
|
||||||
|
(`context.md` §10). All three are offered in every mode; `plan_submit` (`requires: plan`) is the
|
||||||
|
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.
|
`plan` (`permission/defaults.json`), which a rule written for the former `todo` key still reaches.
|
||||||
|
|
||||||
|
## 3.6.0
|
||||||
|
|
||||||
|
- **Phone actions** (`phone.md`, `tools/phone_action.json`, `loop.json` `phone`): a chat's model
|
||||||
|
may do something on the person's phone when they write from the LLeMbas app on it — an alarm or a
|
||||||
|
timer, a calendar event, an SMS or email draft, the dialer, a route (`navigate`, `add_stop`,
|
||||||
|
`search_nearby`, `stop_navigation`), an app or an address, media, the torch, a settings panel,
|
||||||
|
an automation. The app declares the capability `phone_actions` with each message; the person
|
||||||
|
confirms every call on the phone (`confirm: always`); the result is `done`, `declined` or
|
||||||
|
`failed` with what the phone said. The server's alone (`scope: llembas`, `sessions: chat`); new
|
||||||
|
requirement `phone`, offered once a message came from the phone and kept, answering
|
||||||
|
`unavailable` when the message being answered did not. Conformance area **`phone`** (the checks
|
||||||
|
and the result texts).
|
||||||
|
|
||||||
|
|
||||||
|
Written ahead of both implementations; the server vendors it as written, both carry it in the same
|
||||||
|
round (`acp.md` *Scheduled work on a linked computer*, `loop.json` `scheduled`).
|
||||||
|
|
||||||
|
- **Scheduled turns on a linked computer.** A schedule may run each time as an agent turn on a
|
||||||
|
linked device, in a folder of it. The device must allow it: **Accept scheduled work**
|
||||||
|
(`remote.scheduled`, off by default; `lembas service scheduled on|off`), said to the server as
|
||||||
|
`limits.scheduled` and judged again at every request — off, every scheduled request is refused
|
||||||
|
with one fixed text. The server opens an agent session marked `scheduled` (schedule, run, mode,
|
||||||
|
approval wait) with the schedule's Min/Max as an agent chat's, and prompts it with the run's own
|
||||||
|
text. **The mode** is the schedule's — Manual, Edit or Auto, **Edit when none was chosen**, so never
|
||||||
|
wider than Edit unless the person picked Auto; Plan refused — lowered to `remote.max_mode`. The
|
||||||
|
answer reports the run: `status` `ok`, `silent` (the `[SILENT]` sentinel on the first or last
|
||||||
|
line, the server's rule), `failed` or `cancelled`, and the final reply's `text`. A device that is
|
||||||
|
away is waited for like a busy model, then the run is missed; a refusal fails the run.
|
||||||
|
- **Approvals in a scheduled turn** go to the person through the server (a phone notification with
|
||||||
|
the card), carry `scheduled` on `session/request_permission`, and wait the schedule's wait
|
||||||
|
(default 30 minutes) instead of `remote.approval_timeout`; then they count as denied and the turn
|
||||||
|
goes on. `ask_user` and `plan_submit` have nobody to answer; `show` is not offered.
|
||||||
|
- **Monitor mode, the device's half**: `_lembas/monitor` runs a schedule's watch command in its
|
||||||
|
folder, with a timeout, under the mode's permission rules — **a command the mode would ask about
|
||||||
|
is refused** — and answers the output's hash (`sha256:` over the bytes and then the exit status) and a
|
||||||
|
short excerpt; the server runs the model only when the hash changed, and records `unchanged`
|
||||||
|
otherwise.
|
||||||
|
- **Schedules over `/api/v2` with the device's token** (scope `link`): the CLI's `/schedule` lists,
|
||||||
|
creates from words (compile), pauses, resumes, runs now and reads history. A run may be
|
||||||
|
`unchanged`, may wait for the `device`; a schedule may say `runs_on` and `watch`.
|
||||||
|
- **`team_add` under a sub-manager** says where the report goes: `team.texts.added_under` and
|
||||||
|
`added_waiting_under` ("Its report goes to <parent> when it finishes").
|
||||||
|
- Capability `scheduled`. Conformance areas **`scheduled`** (the mode, the wait, the refusals),
|
||||||
|
**`silent`** (the sentinel and the stripped text) and **`monitor`** (the hash, and what a watch
|
||||||
|
command may run); `team` cases changed for `added_under`.
|
||||||
|
|
||||||
|
## 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. **An agent chat's subagents are the device's** (`subagents.md`
|
||||||
|
§14, `acp.md` *Subagents in an agent chat*): capability `agents` carries the chat's `{ min, max }`
|
||||||
|
down on every turn (and `said`, the person's earlier words, on `session/new`); the tree, progress,
|
||||||
|
approvals, findings and transcripts come up. 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`).
|
||||||
|
|
||||||
|
- **Subagent** is the word everywhere a person or a model reads one — a `task` subagent and a
|
||||||
|
team's member alike; *helper* is retired. Tool names and aliases are unchanged. The `team`
|
||||||
|
conformance area's step `helper_done` is now **`task_done`**.
|
||||||
|
- **The model**: a subagent runs on the session's model, unless the person named another one in
|
||||||
|
this session's own messages (its served id, its id at its connection or its display name, a whole
|
||||||
|
word, without case — never the model's text or a tool's output). `task`'s and `team_add`'s
|
||||||
|
`model` is refused otherwise: "Use another model for subagents only when the person names it."
|
||||||
|
No setting chooses a subagent's model; an agent definition's own `model` is not used for one.
|
||||||
|
- **Capacity, the logical restrictions only**: another model on the session's model's one-model
|
||||||
|
server is refused ("That model is on the same one-model server as this chat's model; name a
|
||||||
|
model on another server."); the session's model as its own subagent needs `parallel ≥ 2` or no
|
||||||
|
limit, else "This model serves one request at a time: name another model, on another server,
|
||||||
|
for subagents." — and nothing else is picked. A member is refused for these too, and still only
|
||||||
|
waits for a full model.
|
||||||
|
- **The offer**: the `agents {min, max}` setting is shown as **Subagents** (**Min**, **Max**;
|
||||||
|
`loop.json` `subagents.setting`) — one range from 1 to the cap with two handles, and a hint that
|
||||||
|
the numbers count the session's own agent: 1 is the agent alone, Max 4 the agent and up to three
|
||||||
|
subagents — and with it the team tools, when subagents are enabled and at
|
||||||
|
least one model could serve: the session's, with the capacity, or any usable model off its
|
||||||
|
one-model server. `context.note.team_own` tells the model, and `subagents.setting.own` the
|
||||||
|
person, when the session's model cannot serve subagents itself.
|
||||||
|
- `task`'s and `team_add`'s `model` descriptions, `task`'s description (a line on the model),
|
||||||
|
`models/schema.json` capacity texts, `team.md` §2, §4, §8. Conformance area **`subagents`**.
|
||||||
|
|
||||||
|
## 3.2.0
|
||||||
|
|
||||||
|
Written ahead of both implementations; both carry it in the same round.
|
||||||
|
|
||||||
|
- **Effort levels and the thinking switch** (`models/schema.json`). `thinking_switch`: the model's
|
||||||
|
server turns thinking off for `chat_template_kwargs.enable_thinking: false`. The level `off`
|
||||||
|
exists only then, and sends that and no effort — before, `off` sent nothing, which left the model
|
||||||
|
at its server's default effort. A model that takes no efforts has the one level `default`, which
|
||||||
|
sends nothing. The levels a chat offers are `off` (with the switch) and the efforts; one level
|
||||||
|
shows no control, `[off, X]` a Thinking on/off switch, more a slider. `capacity.parallel`: how
|
||||||
|
many requests the model serves at once (`single_session` is `parallel: 1`). Conformance area
|
||||||
|
`levels`; `quirks.json` `thinking-default`; in `acp.md` an `effort` is one of these levels.
|
||||||
|
- **Reasoning goes back during its turn** (`context.md` §8): every assistant message after the last
|
||||||
|
message that started a turn carries its reasoning (`reasoning_content`; Anthropic thinking
|
||||||
|
blocks); earlier turns carry none. Area `reasoning`.
|
||||||
|
- **No absolute paths in the prompt** (`context.md` §11): an agent session's environment block names
|
||||||
|
the project and says paths are relative to its root (`loop.json` `context.env`); relative paths
|
||||||
|
resolve against the project root, absolute ones are still taken. Area `paths`.
|
||||||
|
- **Teams** (`team.md`): `team_add`, `team_send`, `team_read`, `team_stop` (shared, both,
|
||||||
|
`requires: team`); the chat setting `agents {min, max}`; the room and its marks; reports to the
|
||||||
|
manager; the size across the tree, `task` helpers included; a manager's work tools refused with a
|
||||||
|
fixed text while the tool list stays the same; members waiting for a slot past the model's
|
||||||
|
capacity; `blocks/team_lead.md` and `blocks/team_member.md`; size notes in `context.note`. Area
|
||||||
|
`team`.
|
||||||
|
|
||||||
|
## 3.1.0
|
||||||
|
|
||||||
|
Written ahead of both implementations as `3.1.0-draft`, and `3.1.0` once both carried it (LLeMbas CLI 8e06590, LLeMbas 48e905f). **The prefix stays still** (`context.md` §10). Found on 2026-11-05 by measuring a local model's
|
||||||
|
prompt cache: a harness that rebuilds its system prompt for every request re-reads the whole session
|
||||||
|
whenever any part of it changes — the date at midnight, a permission mode, a memory, the plan. So:
|
||||||
|
the system prompt is made once per session and resent byte for byte; no clock is in it; what
|
||||||
|
changes during a session comes as a `<session-note>` on the next user message (`loop.json`
|
||||||
|
`context.note`); the tool list is fixed for the session (`requires` that can come and go no longer
|
||||||
|
remove a tool — `plan_submit` gains `requires: plan` and an `unavailable` text); compaction asks
|
||||||
|
for its summary as the next turn of the same conversation instead of a separate request with a
|
||||||
|
pasted transcript (§7); every request records its cached input tokens.
|
||||||
|
|
||||||
|
## 3.0.0
|
||||||
|
|
||||||
|
Written ahead of both implementations as `3.0.0-draft`, and `3.0.0` once both carried it (LLeMbas
|
||||||
|
CLI 2.0.0-beta.1, LLeMbas 2.0.0-beta.1). **Two worlds, one harness**: a chat works in the server's file
|
||||||
|
system, an agent session on its device's disk, and neither sees the other's files; what is shared is
|
||||||
|
this design.
|
||||||
|
|
||||||
|
- **`sessions`, `requires`, `unavailable`, `confirm`** — new fields on a tool (Vocabulary). Every
|
||||||
|
tool says its `sessions`.
|
||||||
|
- **New tools.** `mkdir`, `mv`, `rm` (to the trash) and `search` (meaning and words over the files
|
||||||
|
this session can see, filtered by its view after scoring) — LLeMbas, chat. `show` (a file in the
|
||||||
|
user's viewer: Markdown, code, text, HTML in a sandboxed frame) — shared, `requires: display`: a
|
||||||
|
chat in the web UI, an agent session through the link to the web UI or Desktop (`acp.md`
|
||||||
|
`_lembas/show`), never a bare terminal. `python` (Pyodide in the user's browser) — LLeMbas, chat,
|
||||||
|
`requires: page`; with no page it answers *Python cannot run now: no open browser page is attached
|
||||||
|
to this chat.* The forge verbs `commit`, `branch`, `pr`, `merge`, `tag`, `release` — LLeMbas,
|
||||||
|
chat, `requires: forge`; `merge`, `tag` and `release` `confirm: always`. `recall` — shared, both
|
||||||
|
(`context.md`). An agent session keeps git through `bash`, as before.
|
||||||
|
- **The file tools are shared.** `read`, `write`, `edit`, `glob`, `grep` and `list` are `shared` and
|
||||||
|
offered in both: on the device's disk, and over the server's tree in a chat. `multiedit`,
|
||||||
|
`apply_patch` and `bash` stay the agent's. A chat's listing marks a mount and a read-only entry
|
||||||
|
(`filesystem.md`, told to the model in `blocks/workspace.md`). `skills_list`, `skill_view` and `skill_manage` work over skill files on both
|
||||||
|
sides; `memory` is the same tool on both, and its records are the server's for both
|
||||||
|
(`filesystem.md`, *Memory*; `acp.md`, *The HTTP API*). `session_search` is shared.
|
||||||
|
- **Removed from chat** (LLeMbas): `knowledge_search`, `knowledge_get`, `notes_search`,
|
||||||
|
`note_view`, `note_manage` — now `cli` and `agent` only: the CLI keeps its local library — and
|
||||||
|
LLeMbas's own `report_*`, `scratch_write`, the prototype tools and the canvas, which were never
|
||||||
|
in the spec. Notes, reports and knowledge become files; prototypes become `show`.
|
||||||
|
`blocks/library.md` and `blocks/prototypes.md` are the CLI's alone.
|
||||||
|
- **`filesystem.md`** — the file system as a contract: paths, the kinds `dir`, `file`, `mount`, the
|
||||||
|
levels `none`, `ro`, `stage`, `commit`, grants and floors, the exact error texts, and the rule
|
||||||
|
that a granted path under one that is not is reached only by its full path, its ancestor denied
|
||||||
|
for reading and writing and never listed. Paths absolute in every answer but a result's bracket;
|
||||||
|
one `No such file or directory` for every tool, the `write`/`edit` refusals' texts, the `list`
|
||||||
|
tree; where skills are found and in what order, and the name rule
|
||||||
|
(`^[a-z0-9][a-z0-9-]{0,63}$`). Search results and **indexing by tokens** (at most 480
|
||||||
|
tokens a piece, 64 repeated; a piece the embedder refuses is split, never dropped).
|
||||||
|
**Instruction files stack** from the boundary down to the start directory, outermost first, one
|
||||||
|
per directory (`AGENTS.md`, else `CLAUDE.md`), none from a directory the session cannot read —
|
||||||
|
the CLI took only the nearest one before. `blocks/workspace.md` (LLeMbas) tells a chat's model its
|
||||||
|
view.
|
||||||
|
- **`context.md`** — context hygiene, insert-time only, nothing sent ever rewritten: every result
|
||||||
|
has an id (`#0001`: its number in the session, in hex, at least 4 digits); a read carries its
|
||||||
|
lines and content hash, under the path as the call gave it; an unchanged re-read, or a range
|
||||||
|
inside an earlier unchanged read, is a stub — naming only a result in the request being built; output over 400 lines or 40,000 characters keeps its
|
||||||
|
head and tail with `[N lines elided, full output #id]`; `recall` shows any stored result; edits
|
||||||
|
answer with their diff; the same listing or search with the same result is a stub; compaction
|
||||||
|
adds a table of the reads it covers (those since the previous compaction, in turns still in the
|
||||||
|
history) and empties the dedupe table; earlier reasoning is left to the chat
|
||||||
|
template. `loop.json` `context` holds the texts and numbers, `blocks/context.md` tells the model.
|
||||||
|
- **`acp.md`**: **link protocol 3** — LLeMbas 2.0 and LLeMbas CLI 2.0 speak it alone, and a 1.x peer
|
||||||
|
and a 2.0 one refuse each other, each saying what to update (discovery's `protocol` is raised to 3
|
||||||
|
on purpose); the served id is the device's ref for a model (one server, no connection prefix);
|
||||||
|
`session/new`'s result carries `instance`; a session the client opened runs on the
|
||||||
|
chat's model and no other — refused rather than answered on the device's own, so the "answered on
|
||||||
|
its own model" notice is gone; capability `show` with `_lembas/show`; memory through the
|
||||||
|
instance; a 3.0.0 instance sends neither `library` nor `prototypes`. `GET /v1/usage` counts
|
||||||
|
and prices without credits: the month's cost per currency (`month.cost_by_currency`), never
|
||||||
|
summed across currencies; the credit fields are null. The device asks for the `memory` scope.
|
||||||
|
- **Permissions**: default rules for the new tools (`search`, `recall`, `show`, `python` allowed;
|
||||||
|
the forge verbs asked); `modes.json` `confirm`.
|
||||||
|
- **Conformance**: `fs_access`, `instructions`, `context`, `chunk_tokens` — new, their answers
|
||||||
|
written from a reference model of the rules, not recorded from either implementation (neither has
|
||||||
|
them yet). `chunk` stays, noted as the character rule.
|
||||||
|
|
||||||
## 2.0.0
|
## 2.0.0
|
||||||
|
|
||||||
Background work is one thing in both projects: a **job**, owned by a session, is a command run in
|
Background work is one thing in both projects: a **job**, owned by a session, is a command run in
|
||||||
@@ -126,7 +363,9 @@ narrower tool's facts through its `variables`.
|
|||||||
## Changing it
|
## Changing it
|
||||||
|
|
||||||
1. Change the spec first, and bump `VERSION`: **patch** for a text or a new conformance case,
|
1. Change the spec first, and bump `VERSION`: **patch** for a text or a new conformance case,
|
||||||
**minor** for a new tool, rule or field, **major** for a rename or a removed tool. A tool folded
|
**minor** for a new tool, rule or field, **major** for a rename or a removed tool — removed
|
||||||
|
from either project. A spec written ahead of its implementations carries a pre-release suffix
|
||||||
|
(`3.0.0-draft`) until both have it. A tool folded
|
||||||
into another, its name kept as an alias that still runs, is minor: nothing stored or trained
|
into another, its name kept as an alias that still runs, is minor: nothing stored or trained
|
||||||
breaks.
|
breaks.
|
||||||
2. A fix comes with a **conformance case** that fails without it. In LLeMbas CLI,
|
2. A fix comes with a **conformance case** that fails without it. In LLeMbas CLI,
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.0.0
|
3.6.0
|
||||||
+364
-64
@@ -13,24 +13,31 @@ change to the protocol is a change here first, then in both projects.
|
|||||||
## Versions and discovery
|
## Versions and discovery
|
||||||
|
|
||||||
- **`protocol`** (`_meta.lembas.protocol` in `initialize`, `protocol` in `_lembas/hello`):
|
- **`protocol`** (`_meta.lembas.protocol` in `initialize`, `protocol` in `_lembas/hello`):
|
||||||
**2**. CLI `src/acp/agent.ts:LEMBAS_PROTOCOL`, LLeMbas
|
**3**. CLI `src/acp/agent.ts:LEMBAS_PROTOCOL`, LLeMbas
|
||||||
`services/device_link.py:PROTOCOL`.
|
`services/device_link.py:PROTOCOL`.
|
||||||
- **Discovery** (`/.well-known/lembas.json`) says `protocol: 1` **and** `protocols: [1, 2]` —
|
- **3 is LLeMbas 2.0 and LLeMbas CLI 2.0, and they speak it alone** (spec 3.0.0). A 2.0 server keeps
|
||||||
`protocol` stays 1 on purpose, because a client that compares it for equality at login must
|
the server's file system, memory and models, and a 2.0 CLI talks to nothing else, so a 1.x peer
|
||||||
still get in. The CLI reads `protocols` (falling back to `protocol` where it is absent) at
|
and a 2.0 one **refuse each other**, each saying what to update:
|
||||||
login and again **every time the link dials**, keeps it with the login, and says protocol 2 in
|
- **Discovery** (`/.well-known/lembas.json`) of a 2.0 server says `protocol: 3` **and**
|
||||||
`hello` and `initialize` only to an instance that lists 2; to any other it says 1 (an instance that
|
`protocols: [3]`. `protocol` was frozen at 1 for 1.x so that a client comparing it for equality
|
||||||
speaks only 1 closes a link whose hello says 2 with **4400**). A link closed with 4400 stops — it is not
|
could still log in; 3 raises it on purpose, so a 1.x CLI's login is refused there (it says the
|
||||||
redialled — saying *the instance and this CLI speak different link protocols — update one of them*.
|
server speaks protocol 3 and to update the older of the two).
|
||||||
- **A protocol 1 peer keeps working.** Every feature beyond protocol 1 is named by a **capability
|
- A **2.0 CLI** logs in only where discovery lists 3 (`protocols`, falling back to `protocol`).
|
||||||
string** in `initialize`'s `_meta.lembas.capabilities`, and **LLeMbas never calls a method, nor
|
To a server that does not, at login and again at every dial of the link, it says *<server>
|
||||||
relies on a field, the device did not list**. The CLI logs in to an instance whose discovery says
|
runs LLeMbas X, which speaks link protocol 1 and 2; this LLeMbas CLI needs a LLeMbas 2.0 server
|
||||||
protocol 1 or 2.
|
(protocol 3) — update the server, or use LLeMbas CLI 1.x with it*, and the link stops.
|
||||||
|
- A **2.0 server** closes a link whose hello says 1 or 2 with **4400** and the reason *update
|
||||||
|
LLeMbas CLI to 2.0*; a 1.x CLI stops on 4400 without redialling. A 2.0 CLI's hello says 3, and
|
||||||
|
a 1.x server closes it with 4400 likewise; the CLI stops and says the server needs updating.
|
||||||
|
- **Within protocol 3** every feature is still named by a **capability string** in `initialize`'s
|
||||||
|
`_meta.lembas.capabilities`, and **LLeMbas never calls a method, nor relies on a field, the
|
||||||
|
device did not list**.
|
||||||
- A client says it is LLeMbas with `clientCapabilities._meta.lembas` in `initialize`, and which
|
- A client says it is LLeMbas with `clientCapabilities._meta.lembas` in `initialize`, and which
|
||||||
protocol it speaks with `_meta.lembas.protocol` (absent: 1 — a protocol 1 client sends `lembas: true`).
|
protocol it speaks with `_meta.lembas.protocol` (absent: 1 — a protocol 1 client sends `lembas: true`).
|
||||||
Only such a client gets `_lembas/event`; only one that says protocol **2 or later** gets
|
Only such a client gets `_lembas/event`; only one that says protocol **2 or later** gets
|
||||||
`_lembas/question` and `_lembas/plan`, and one that then answers them with method-not-found is
|
`_lembas/question` and `_lembas/plan` (a 2.0 server says 3), and one that then answers them with
|
||||||
treated as protocol 1 for the rest of the link.
|
method-not-found is treated as protocol 1 for the rest of the link — what an editor on
|
||||||
|
`serve --stdio` is.
|
||||||
|
|
||||||
| Capability | What it promises |
|
| Capability | What it promises |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -52,17 +59,22 @@ change to the protocol is a change here first, then in both projects.
|
|||||||
| `turns` | `_meta.lembas.turnId` on `session/prompt`, deduplicated; `turnId` on `prompt`/`task` events; turn fields in `status` |
|
| `turns` | `_meta.lembas.turnId` on `session/prompt`, deduplicated; `turnId` on `prompt`/`task` events; turn fields in `status` |
|
||||||
| `shell` | `shell` and `integration` in `_lembas/terminal/open`'s answer (listed only with `terminal`) |
|
| `shell` | `shell` and `integration` in `_lembas/terminal/open`'s answer (listed only with `terminal`) |
|
||||||
| `context` | `system_prompt` and `personalization` in `session/new` and `session/prompt` `_meta.lembas` are read |
|
| `context` | `system_prompt` and `personalization` in `session/new` and `session/prompt` `_meta.lembas` are read |
|
||||||
| `library` | `library` in `session/new` and `session/prompt` `_meta.lembas` is read; `_meta.lembas.scope` sent on the instance library's `tools/call` |
|
| `library` | `library` in `session/new` and `session/prompt` `_meta.lembas` is read; `_meta.lembas.scope` sent on the instance library's `tools/call`. Spec 3.0.0: an instance's chats have no knowledge bases, so a 3.0.0 instance sends no `library`; the scope is still sent on its `memory` calls (*The HTTP API*) |
|
||||||
| `prototypes` | `prototypes` in `session/new` and `session/prompt` `_meta.lembas` is read |
|
| `prototypes` | `prototypes` in `session/new` and `session/prompt` `_meta.lembas` is read. Spec 3.0.0: a 3.0.0 instance sends none (`show` replaces prototypes) |
|
||||||
| `plan_state` | the structured plan: `_meta.lembas.plan` on the `plan` session update and on `_lembas/plan`; `plan` in `_lembas/session/status` |
|
| `plan_state` | the structured plan: `_meta.lembas.plan` on the `plan` session update and on `_lembas/plan`; `plan` in `_lembas/session/status` |
|
||||||
| `rewind` | `_lembas/session/rewind` |
|
| `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 |
|
| `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` |
|
||||||
|
| `agents` | spec 3.4.0: the session's Subagents setting `{ min, max }` is read from `_meta.lembas.agents` on `session/new`, `session/prompt` and `_lembas/configure` (*Subagents in an agent chat*, below) |
|
||||||
|
| `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` |
|
||||||
|
| `scheduled` | spec 3.5.0 (*Scheduled work on a linked computer*, below): `scheduled` in `_meta.lembas` of `session/new` and `session/prompt` read, and the run's outcome in the prompt's answer; `scheduled` on `session/request_permission`; `_lembas/monitor`; `limits.scheduled` in `_lembas/hello` and `initialize`. Listed by every build that has it, whatever the setting says: whether this device *accepts* scheduled work is `limits.scheduled` |
|
||||||
|
|
||||||
## Standard ACP, as used here
|
## Standard ACP, as used here
|
||||||
|
|
||||||
### `initialize` — client → agent
|
### `initialize` — client → agent
|
||||||
|
|
||||||
Params: `{ protocolVersion: 1, clientCapabilities: { _meta?: { lembas?: { protocol: 2 } | true } } }`.
|
Params: `{ protocolVersion: 1, clientCapabilities: { _meta?: { lembas?: { protocol: 3, show?: true } | true } } }`.
|
||||||
|
|
||||||
Result:
|
Result:
|
||||||
|
|
||||||
@@ -75,44 +87,52 @@ Result:
|
|||||||
"mcpCapabilities": { "http": false, "sse": false }
|
"mcpCapabilities": { "http": false, "sse": false }
|
||||||
},
|
},
|
||||||
"authMethods": [],
|
"authMethods": [],
|
||||||
"agentInfo": { "name": "LLeMbas CLI", "version": "1.3.0" },
|
"agentInfo": { "name": "LLeMbas CLI", "version": "2.0.0" },
|
||||||
"_meta": { "lembas": {
|
"_meta": { "lembas": {
|
||||||
"protocol": 2,
|
"protocol": 3,
|
||||||
"harness_spec": "1.1.0",
|
"harness_spec": "3.0.0",
|
||||||
"limits": { "roots": ["~/work"], "max_mode": "edit" },
|
"limits": { "roots": ["~/work"], "max_mode": "edit", "scheduled": false },
|
||||||
"connection": "example",
|
"connection": "example",
|
||||||
"capabilities": ["directories", "configure", "…"]
|
"capabilities": ["directories", "configure", "…"]
|
||||||
} }
|
} }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`limits` is `null` on `serve --stdio`. `connection` is the device's name for the instance — the
|
`limits` is `null` on `serve --stdio`. `limits.scheduled` (spec 3.5.0, capability `scheduled`): whether
|
||||||
login's connection — or `null`; an instance that does not speak protocol 2 recognises its own
|
this device accepts scheduled work now (`remote.scheduled`, *Scheduled work on a linked computer*). `connection` is the device's name for the instance — the
|
||||||
models by it, as `<connection>/<name>`, while the `model` event and `announce` say it directly
|
login's connection — or `null`; the `model` event and `announce` say directly whether a model is
|
||||||
(`instance`).
|
the instance's (`instance`).
|
||||||
|
|
||||||
**How the device names an instance's models.** An instance that speaks protocol 2 serves every model
|
**How the device names an instance's models.** The served id **is the device's ref** for a model
|
||||||
as `<provider>/<model>` (`deepseek/deepseek-flash`), and that served id **is the device's ref** for
|
(`deepseek/deepseek-flash`; spec 3.0.0: a 2.0 device talks to one server, the one it is logged in
|
||||||
it — each provider is, as the person sees it, a connection of its own, backed by the instance
|
to, so there is nothing to tell apart — no connection prefix, no second login, no connection of
|
||||||
(requests still go to its `/v1`, with the served id). Exceptions: a connection of the device's own
|
the device's own). The older forms (`<login>/<id>`, and from before providers `<login>/<model>` for
|
||||||
with the provider's name keeps the short form, and the instance's model is then
|
the one served `<provider>/<model>` of that name) still name the same model. `lembas_model` in
|
||||||
`<login>/<provider>/<model>`; with two instances serving one provider, the one logged in to first
|
`session/new` and `model` in `_lembas/configure` are the instance's served name, as it serves it.
|
||||||
keeps the short form and the other's are `<login>/<provider>/<model>`; an instance that does not
|
`model` in a `model` event and in a result is the device's ref, which is the served id.
|
||||||
speak protocol 2 serves bare ids, which stay `<login>/<id>`. The provider is the model's `provider` field where the
|
|
||||||
instance gives one, else — only from an instance whose discovery lists protocol 2 — the part of the
|
|
||||||
id before its first `/`; an id with a `/` from any other instance is just an id (`<login>/<id>`). The old forms (`<login>/<model>`,
|
|
||||||
`<login>/<provider>/<model>`) still name the same model. `lembas_model` in `session/new` and
|
|
||||||
`model` in `_lembas/configure` are the instance's served name, as it serves it.
|
|
||||||
|
|
||||||
### `session/new` — client → agent
|
### `session/new` — client → agent
|
||||||
|
|
||||||
Params: `{ cwd: <absolute>, mcpServers?: [], _meta?: { mode?, lembas_model?, model?, effort?, lembas?: Context } }`.
|
Params: `{ cwd: <absolute>, mcpServers?: [], _meta?: { mode?, lembas_model?, model?, effort?, lembas?: Context } }`.
|
||||||
`cwd` must be a directory inside `remote.roots`, in a trusted project when `remote.require_trust`
|
`cwd` must be a directory inside `remote.roots`, in a trusted project when `remote.require_trust`
|
||||||
(the default) — otherwise `-32602` with the reason. `mode` is lowered to `remote.max_mode`.
|
(the default) — otherwise `-32602` with the reason. `mode` is lowered to `remote.max_mode`.
|
||||||
`lembas_model` is the instance's name of a model (it becomes `<connection>/<name>` here); `model`
|
`lembas_model` is the instance's name of a model; `model` a ref the device knows (an editor's
|
||||||
a ref of the device's own; `effort` a level or `"off"`.
|
choice); `effort` one of the model's levels (`models/schema.json` `levels`, spec 3.2.0: `"off"` where the model has `thinking_switch`, `"default"` where it takes no efforts, else an effort); a level the model does not offer is read as its start level, and the result says the level used.
|
||||||
|
|
||||||
Result: `{ sessionId: "ses_…", modes: { currentModeId, availableModes }, _meta: { lembas: { model, mode, effort } } }`.
|
Result: `{ sessionId: "ses_…", modes: { currentModeId, availableModes }, _meta: { lembas: { model, mode, effort, instance } } }`.
|
||||||
|
`instance` (spec 3.0.0) is a boolean, with the `model` event's meaning: true exactly when the
|
||||||
|
session's model is one this link's instance serves.
|
||||||
|
|
||||||
|
**The chat's model, full stop** (spec 3.0.0). A session the client opens with `lembas_model` runs on
|
||||||
|
**that** model and on no other: the device takes it as given, for every turn of the session, until
|
||||||
|
the client changes it (`_lembas/configure`). If the device cannot speak to it — the instance does not
|
||||||
|
serve it to this device, or this device is not logged in to the link's instance — `session/new`
|
||||||
|
fails with `-32602` and the reason (so does a `_lembas/configure` naming such a model, and the
|
||||||
|
session keeps the model it had), and a turn that loses the model midway fails with its error; the
|
||||||
|
device never answers on a model of its own instead, and never falls back on its own. So `instance`
|
||||||
|
is true for every session the client opened, and a client has nothing to say about a device having
|
||||||
|
"answered on its own model": that notice is gone. A session a terminal shares (`announce`) keeps the
|
||||||
|
terminal's model, and its `instance` says which it is.
|
||||||
The id is the device's store id and lasts: a session the client names later that is not in memory
|
The id is the device's store id and lasts: a session the client names later that is not in memory
|
||||||
is opened from the store under the same rules (capability `sessions`). With capability `commands`,
|
is opened from the store under the same rules (capability `sessions`). With capability `commands`,
|
||||||
an `available_commands_update` follows the answer.
|
an `available_commands_update` follows the answer.
|
||||||
@@ -230,11 +250,197 @@ 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
|
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.
|
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
|
`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.
|
||||||
|
`scheduled` (capability `scheduled`, spec 3.5.0): `{ schedule_id, run_id, wait_s }` when the call is
|
||||||
|
a scheduled turn's — nobody is at the chat, so the client puts the card to the person where they are
|
||||||
|
(LLeMbas: a notification on their phone carrying the card, answerable there and in every client),
|
||||||
|
and the device waits `wait_s` for it, not `remote.approval_timeout` (*Scheduled work*, below).
|
||||||
Answer: ACP's `{ outcome: { outcome: "selected", optionId } | { outcome: "cancelled" } }`, with
|
Answer: ACP's `{ outcome: { outcome: "selected", optionId } | { outcome: "cancelled" } }`, with
|
||||||
optional `_meta.lembas.command` (the corrected command line, `once` only) or
|
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.
|
`_meta.lembas.feedback` (why it was denied). No answer within `remote.approval_timeout` is a denial.
|
||||||
|
|
||||||
|
## Subagents in an agent chat (spec 3.4.0)
|
||||||
|
|
||||||
|
**An agent chat's main agent is the CLI on the device.** Its subagents are the device's: it starts
|
||||||
|
them locally, on the models of the server it is logged in to, under every rule of `subagents.md` —
|
||||||
|
the model rules (§2–§3), Min/Max, slots, permissions only ever narrowed, request options, and a
|
||||||
|
call nobody in the tree can allow going to the person. The server runs no subagent of its own for
|
||||||
|
an agent chat and offers its own subagent tool to no agent chat; what it does is carry the setting
|
||||||
|
down and the tree up. A device without the capabilities `agents` and `tree` gets neither and runs no
|
||||||
|
subagents for the chat (the chat says so beside the setting).
|
||||||
|
|
||||||
|
**What goes down, with every turn.**
|
||||||
|
|
||||||
|
- `agents: { min, max }` in `_meta.lembas` of `session/new`, of **every** `session/prompt`, and of
|
||||||
|
`_lembas/configure` when the person changes it between turns — the chat's setting as the server
|
||||||
|
keeps it (`team.md` §2: 1 ≤ min ≤ max ≤ the instance's cap). The device takes the value as the
|
||||||
|
session's setting for that turn: a change reaches the main agent as `team.md` §10's note, and
|
||||||
|
nothing else reads the device's own configuration for the chat. Absent (an older server, or a
|
||||||
|
client that is not LLeMbas): the device's own setting, as before 3.4.0. A value the device's own global cap is under is read as `team.md` §2
|
||||||
|
says (lowered, never refused).
|
||||||
|
- `subagents: false` in the same `_meta.lembas` when the instance has subagents switched off: the
|
||||||
|
device offers none whatever `agents` says.
|
||||||
|
- **What the person named** (`subagents.md` §2): the person's words reach the device as the turns'
|
||||||
|
prompt texts, so the device judges a named model from them as in its own sessions — every
|
||||||
|
`session/prompt` without `machine`; never a `machine` turn, a `_lembas/steer` from the harness, or
|
||||||
|
a tool's output. A chat continued on a device that has not seen its earlier turns gets them in
|
||||||
|
`_meta.lembas.said: string[]`, the person's earlier messages of the chat, oldest first (on
|
||||||
|
`session/new` only); the device counts them as typed in the session.
|
||||||
|
- The models: the device's subagents may run on any model of the person's (`/v1/models` as the
|
||||||
|
device's token sees it), as in its own sessions.
|
||||||
|
|
||||||
|
**What comes up.**
|
||||||
|
|
||||||
|
- The tree, as it changes: the `team` event (`{ tree }`, `subagents.md` §12's data, at most four a
|
||||||
|
second), and on request `_lembas/team` (the tree and the whole room). Every agent's `progress`
|
||||||
|
is in it; the main agent's own also as the `phase` event (capability `progress`).
|
||||||
|
- Approvals: a subagent's call that only the person can allow is an ordinary
|
||||||
|
`session/request_permission` with `_meta.lembas.subagent: { id, name, position }`; the server
|
||||||
|
shows it as its usual approval card naming the subagent, in every client. Requests the main agent
|
||||||
|
decides (`team_request` / `team_decide`) never reach the server as approvals: they are room
|
||||||
|
messages.
|
||||||
|
- Findings and transcripts on request: `_lembas/team/findings`, `_lembas/team/transcript` (live and
|
||||||
|
ended subagents, read-only — the link has no method that writes into a subagent's session).
|
||||||
|
- The person stopping a subagent: `_lembas/team/stop`.
|
||||||
|
|
||||||
|
**What the server keeps.** The latest tree with the chat (so a client opening the chat draws it at
|
||||||
|
once, and after the device has gone), and the room as `_lembas/team` last answered. Findings and
|
||||||
|
transcripts stay on the device; a client asking for one while the device is not linked is told it
|
||||||
|
is on the device, and gets it once the device links again. Over `/api/v2` the chat's stream carries
|
||||||
|
the tree as it changes and the endpoints to read a subagent's transcript and findings, the same for
|
||||||
|
a chat's own subagents and for an agent chat's — so Web, Desktop and Android draw both alike.
|
||||||
|
|
||||||
|
## Scheduled work on a linked computer (spec 3.5.0)
|
||||||
|
|
||||||
|
A schedule (the server's: its rule, its runs, its history and notifications) may run **on a linked
|
||||||
|
computer**: each run is an agent turn on that device, in a folder of it, instead of a chat on the
|
||||||
|
server. The server decides *when*; the device decides *whether*, and *how far* — every rule an agent
|
||||||
|
chat has still holds, and the ones below are added. Numbers and texts: `loop.json` `scheduled`.
|
||||||
|
Conformance areas `scheduled`, `silent`, `monitor`.
|
||||||
|
|
||||||
|
### The device's setting: Accept scheduled work
|
||||||
|
|
||||||
|
- **`remote.scheduled`** in the device's global config, **off by default**; shown as **Accept
|
||||||
|
scheduled work** (`loop.json` `scheduled.setting`). Set from the command line with
|
||||||
|
`lembas service scheduled on|off` (also `lembas remote scheduled on|off`; `lembas remote list
|
||||||
|
--json` says `scheduled`), and by LLeMbas Desktop through the same command. It is global only: a
|
||||||
|
project's config cannot turn it on.
|
||||||
|
- It narrows remote work and widens nothing: a scheduled turn also needs `remote.enabled`, a folder
|
||||||
|
inside `remote.roots`, a trusted project where `remote.require_trust`, and a mode within
|
||||||
|
`remote.max_mode`, exactly as an agent chat does.
|
||||||
|
- The device says it in `limits.scheduled` (hello and `initialize`). Turning it on or off changes
|
||||||
|
the limits, so the device dials again once nothing works (as for any limit) and the client hears
|
||||||
|
it. The client may offer the device in a schedule's form only while `limits.scheduled` is true,
|
||||||
|
and says why not otherwise. The device judges it again at every `session/new`, `session/prompt`
|
||||||
|
and `_lembas/monitor` that is scheduled, whatever it said earlier.
|
||||||
|
- **Off** (or remote work off): every scheduled request is refused with `-32602`, the message
|
||||||
|
`loop.json` `scheduled.texts.off` word for word, and `data: { reason: "scheduled_off" }`. Not
|
||||||
|
retried: it is the person's setting. A folder the device does not accept is `-32602` with the
|
||||||
|
usual reason and `data: { reason: "folder" }`.
|
||||||
|
|
||||||
|
### Starting a scheduled turn
|
||||||
|
|
||||||
|
The client opens an agent session as for an agent chat (`session/new`: `cwd`, `lembas_model`,
|
||||||
|
`effort`, `_meta.lembas` with `agents` — the schedule's **Min/Max**, as an agent chat's — and the
|
||||||
|
`Context`), marked scheduled:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"_meta": { "mode": "edit", "lembas_model": "qwen", "lembas": {
|
||||||
|
"agents": { "min": 1, "max": 4 },
|
||||||
|
"scheduled": { "schedule_id": "sched1", "run_id": "run7", "title": "Waybread check",
|
||||||
|
"mode": "edit", "approval_wait_s": 1800 } } }
|
||||||
|
```
|
||||||
|
|
||||||
|
and runs the turn with `session/prompt`, its `_meta.lembas` carrying the same `scheduled` object
|
||||||
|
(and a `turnId`, the run's — so a prompt sent again after a dropped link gets the run's answer
|
||||||
|
instead of a second run). A schedule that continues one conversation (`continue`) prompts the same
|
||||||
|
session again on its next run; a fresh run opens a new one. The prompt's text is the server's own
|
||||||
|
for a scheduled run — the instruction, the run's time, and for a schedule that notifies only when
|
||||||
|
it matters the `[SILENT]` hint — composed exactly as for a run on the server; the device adds nothing.
|
||||||
|
|
||||||
|
**The mode** (`scheduled.mode`; `_meta.mode` is ignored for a scheduled session): `manual`, `edit`
|
||||||
|
or `auto`, and **`edit` when absent** — so a run is never wider than Edit unless the person picked
|
||||||
|
Auto on that schedule. `plan` is refused (`scheduled.texts.plan`, `data: { reason: "plan" }`):
|
||||||
|
nobody is there to approve a plan. Then lowered to `remote.max_mode`, as every remote session is —
|
||||||
|
Auto runs only where both the schedule and the device allow it (`conformance/scheduled.json`). The
|
||||||
|
mode holds for the turn; neither `_lembas/configure` nor a command in the prompt widens it.
|
||||||
|
|
||||||
|
In a scheduled turn:
|
||||||
|
|
||||||
|
- **Approvals** go to the person through the client (`session/request_permission` with
|
||||||
|
`_meta.lembas.scheduled: { schedule_id, run_id, wait_s }`), and the device waits
|
||||||
|
`approval_wait_s` — the schedule's wait, 30 minutes by default, clamped to
|
||||||
|
`scheduled.approval_wait_seconds` — instead of `remote.approval_timeout`. Unanswered by then, the
|
||||||
|
call is denied with `scheduled.texts.denied` as the feedback the model reads (`{{minutes}}` the
|
||||||
|
wait in whole minutes), the card is settled (`_lembas/permission/settled`), and the turn goes on —
|
||||||
|
the model works around the refusal or fails. Not `patient`.
|
||||||
|
- **`ask_user` and `plan_submit`** have nobody to answer: the device answers them as it does with
|
||||||
|
no client (`ask_user` tells the model nobody can be asked; `plan_submit` saves the plan and says
|
||||||
|
to stop). `show` is not offered: nobody is looking.
|
||||||
|
- Subagents as in an agent chat (`agents`, the tree, findings); a subagent's call only the person
|
||||||
|
can allow is an approval like the main agent's, with `scheduled` beside `subagent`.
|
||||||
|
|
||||||
|
### How the turn ends and reports
|
||||||
|
|
||||||
|
`session/prompt`'s answer carries the run's outcome in `_meta.lembas.scheduled`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "stopReason": "end_turn", "_meta": { "lembas": { "turnId": "run7",
|
||||||
|
"scheduled": { "run_id": "run7", "status": "ok", "text": "All the waybread is wrapped.",
|
||||||
|
"error": null } } } }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`text`**: the turn's final reply — the text of the last assistant message the turn wrote, as
|
||||||
|
written (`[SILENT]` included). Model output: untrusted wherever it is shown.
|
||||||
|
- **`status`**: `cancelled` — the turn was cancelled (`session/cancel`: the person stopped it, or the
|
||||||
|
client's inactivity limit); `failed` — the turn ended on an error (`error` its text), or with no
|
||||||
|
reply text at all (`scheduled.texts.no_reply`); `silent` — the reply says the sentinel
|
||||||
|
`[SILENT]` on its first or last non-empty line (any case, a trailing full stop forgiven;
|
||||||
|
`conformance/silent.json`, the server's own rule for its runs); otherwise `ok`. A turn that hit the
|
||||||
|
step backstop or a budget and still wrote a reply is `ok`; `stopReason` says why it stopped.
|
||||||
|
- The client records the run from this (its history, its notification by the schedule's policy —
|
||||||
|
`silent` is never notified, a failure is told as a failure), and keeps the session as the run's
|
||||||
|
agent chat, whose events it received as for any agent chat. What the sentinel stripped looks like
|
||||||
|
is `silent.json`'s `stripped`.
|
||||||
|
|
||||||
|
### When the device is away, or refuses
|
||||||
|
|
||||||
|
- **Away** (not linked when the run is due, or the link drops before `session/new` is answered): the
|
||||||
|
client treats the device like a busy model — the run is `waiting` (`waiting_for: "device"`) and
|
||||||
|
tried again on the same steps a run waiting for a free model is, until the run's window closes;
|
||||||
|
then it is **missed**. A turn already running when the link drops is followed again by
|
||||||
|
`_lembas/session/status` and the turn id, as for any agent chat.
|
||||||
|
- **Refused** (`-32602` from a scheduled `session/new`, `session/prompt` or `_lembas/monitor`): the
|
||||||
|
run **fails** with the device's message, said in the run's history and in the failure
|
||||||
|
notification; `data.reason` (`scheduled_off`, `folder`, `plan`, `permission`) lets the client say
|
||||||
|
what to change. Not retried.
|
||||||
|
|
||||||
|
### Monitor mode: a command watched on the device
|
||||||
|
|
||||||
|
A schedule may **watch** something and run the model only when it changed. A URL is fetched by the
|
||||||
|
server and needs nothing from the device; a **command's output** is watched on the device with
|
||||||
|
`_lembas/monitor` (below): the client asks the device to run the watch command, compares the
|
||||||
|
answer's `hash` with the one the previous check stored, and when it is the same records the run as
|
||||||
|
`unchanged` — no model, nobody notified. Changed (or the first check), the model runs as the
|
||||||
|
schedule says — on the server, or as a scheduled turn on the device — told what changed through the
|
||||||
|
`excerpt`. A check that fails (timed out, refused) is a failed run.
|
||||||
|
|
||||||
|
The command runs under the **same permission rules as the schedule's mode**, with one difference:
|
||||||
|
**a command the mode would ask about is refused**, because a watch has nobody to ask. The device
|
||||||
|
judges the command line as `bash` would be judged in a session in that folder (the hardline, the
|
||||||
|
default rules, the global rules and — in a trusted project — the project's), and runs it only on
|
||||||
|
`allow`; `ask` is refused with `scheduled.texts.monitor_asks`, `deny` with
|
||||||
|
`scheduled.texts.monitor_denied`, both `-32602` with `data: { reason: "permission" }`
|
||||||
|
(`conformance/monitor.json`).
|
||||||
|
|
||||||
|
**The hash** (`monitor.json` `hash` cases): `sha256:` and the lowercase hex SHA-256 of the
|
||||||
|
output's bytes exactly as read — standard output and standard error as one stream, as the `bash`
|
||||||
|
tool reads them — followed by a newline and the exit status as decimal digits (after the bytes, so
|
||||||
|
the device hashes the output as it streams, however long). Nothing is normalised: a watch command
|
||||||
|
that prints a time changes every time, and should not.
|
||||||
|
|
||||||
## `_lembas/*` methods
|
## `_lembas/*` methods
|
||||||
|
|
||||||
Direction is who sends it. "→ result" marks a request; the others are notifications.
|
Direction is who sends it. "→ result" marks a request; the others are notifications.
|
||||||
@@ -242,7 +448,9 @@ Direction is who sends it. "→ result" marks a request; the others are notifica
|
|||||||
### `_lembas/hello` — agent → client
|
### `_lembas/hello` — agent → client
|
||||||
|
|
||||||
Sent once when the link opens, before `initialize`:
|
Sent once when the link opens, before `initialize`:
|
||||||
`{ protocol, version, harness_spec, device: { host, platform }, limits: { roots, max_mode, require_trust } }`.
|
`{ protocol, version, harness_spec, device: { host, platform }, limits: { roots, max_mode, require_trust, scheduled } }`
|
||||||
|
(`scheduled` since spec 3.5.0: whether the device accepts scheduled work; a change of it is a
|
||||||
|
change of the limits, below).
|
||||||
|
|
||||||
A device links whether its remote work is on or not, and with no roots at all. Off, or with no
|
A device links whether its remote work is on or not, and with no roots at all. Off, or with no
|
||||||
roots, it says `limits.roots: []` here and in `initialize`, and refuses every `session/new`, every
|
roots, it says `limits.roots: []` here and in `initialize`, and refuses every `session/new`, every
|
||||||
@@ -260,7 +468,7 @@ client hears the new limits.
|
|||||||
`{ sessionId, event }`: every engine event of the session, to a client that said `_meta.lembas`.
|
`{ 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`,
|
`event.type` is one of `session`, `prompt`, `step`, `title`, `text`, `reasoning`,
|
||||||
`tool_call_delta`, `progress`, `tool_start`, `tool_end`, `tool_denied`, `usage`, `inbox`,
|
`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:
|
`setting`, `error`, `done` (`src/bus/index.ts`). Those this protocol relies on:
|
||||||
|
|
||||||
| Event | Fields |
|
| Event | Fields |
|
||||||
@@ -269,13 +477,31 @@ client hears the new limits.
|
|||||||
| `task` | `state: "start" \| "end"`, `turnId` — every turn has one: the client's, or one made here for a turn typed in a terminal |
|
| `task` | `state: "start" \| "end"`, `turnId` — every turn has one: the client's, or one made here for a turn typed in a terminal |
|
||||||
| `steered` | `texts`, `ids?` — the `_lembas/steer` message ids taken in at this step |
|
| `steered` | `texts`, `ids?` — the `_lembas/steer` message ids taken in at this step |
|
||||||
| `mode` | `mode` |
|
| `mode` | `mode` |
|
||||||
| `model` | `ref` (the device's ref, above), `effort` (a level or `"off"`), `connection` (the device connection it is spoken to through) and **`instance: boolean`** — true exactly when that connection is the login of **this link's** instance, i.e. the model is one the instance serves (its served id is `ref`, or `ref` less a leading `<connection>/`). Whenever either changes, for any reason: `/model`, `/effort`, the settings tool, a fallback, `_lembas/configure` |
|
| `model` | `ref` (the device's ref, above), `effort` (one of the model's levels: an effort, `"off"` or `"default"`), `connection` (the device connection it is spoken to through) and **`instance: boolean`** — true exactly when that connection is the login of **this link's** instance, i.e. the model is one the instance serves (its served id is `ref`, or `ref` less a leading `<connection>/`). Whenever either changes, for any reason: `/model`, `/effort`, the settings tool, a fallback, `_lembas/configure` |
|
||||||
| `plan` | `plan` — the structure, or `null` when cleared (what the `plan` session update carries) |
|
| `plan` | `plan` — the structure, or `null` when cleared (what the `plan` session update carries) |
|
||||||
| `compacted` | `summary` |
|
| `compacted` | `summary` |
|
||||||
| `title` | `id`, `title` |
|
| `title` | `id`, `title` |
|
||||||
| `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` | `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 |
|
| `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 |
|
| `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/monitor` — client → agent → result (capability `scheduled`)
|
||||||
|
|
||||||
|
The watch command of a schedule in monitor mode (*Monitor mode*, above). Params
|
||||||
|
`{ cwd, command, mode?, timeout_s?, schedule_id?, run_id? }` — `cwd` absolute, judged as
|
||||||
|
`session/new`'s; `mode` the schedule's (`manual`, `edit` or `auto`; `edit` when absent; `plan`
|
||||||
|
refused), lowered to `remote.max_mode`; `timeout_s` defaulting to and clamped by
|
||||||
|
`loop.json` `scheduled.monitor.timeout_seconds`. Refused like any scheduled request when the device
|
||||||
|
does not accept scheduled work. The command runs as `bash -c` in `cwd`, in its own process group,
|
||||||
|
with no input; at the timeout the group is stopped.
|
||||||
|
|
||||||
|
Result `{ hash, exit, timed_out, excerpt, truncated, bytes, duration_ms }`: `hash` as above (`null`
|
||||||
|
when it timed out), `exit` the exit status (`null` on a signal or a timeout), `excerpt` the start of
|
||||||
|
the output decoded as UTF-8 (invalid bytes replaced), at most `scheduled.monitor.excerpt_chars`
|
||||||
|
characters, `truncated` whether there was more, `bytes` the output's length. The device hashes all
|
||||||
|
of the output as it streams and keeps at most `scheduled.monitor.output_max_bytes` of it.
|
||||||
|
|
||||||
### `_lembas/directories` — client → agent → result
|
### `_lembas/directories` — client → agent → result
|
||||||
|
|
||||||
@@ -286,7 +512,7 @@ Only on the link (an error on `serve --stdio`).
|
|||||||
|
|
||||||
### `_lembas/configure` — client → agent → result
|
### `_lembas/configure` — client → agent → result
|
||||||
|
|
||||||
Params `{ sessionId, model?: <instance's name>, effort?: <level> | "off" }` between prompts. Result
|
Params `{ sessionId, model?: <instance's name>, effort?: <level>, agents?: { min, max } }` between prompts (a level as in `session/new`; `agents` with capability `agents`, spec 3.4.0). Result
|
||||||
`{ model: <ref>, effort }`. Emits a `model` event when something changed.
|
`{ model: <ref>, effort }`. Emits a `model` event when something changed.
|
||||||
|
|
||||||
### `_lembas/steer` — client → agent → result
|
### `_lembas/steer` — client → agent → result
|
||||||
@@ -304,9 +530,9 @@ with the message in the store; `attachments`, and the text without them) or `{ r
|
|||||||
### `_lembas/session/status` — client → agent → result
|
### `_lembas/session/status` — client → agent → result
|
||||||
|
|
||||||
Params `{ sessionId }`. 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
|
— `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:
|
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
|
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).
|
`history`; T unknown → send the prompt again (safe: the device deduplicates).
|
||||||
@@ -347,6 +573,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
|
`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.
|
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
|
### `_lembas/session/delete` — client → agent → result
|
||||||
|
|
||||||
Params `{ sessionId }`: the chat was deleted. The session stops if it works, leaves the store (its
|
Params `{ sessionId }`: the chat was deleted. The session stops if it works, leaves the store (its
|
||||||
@@ -375,6 +625,18 @@ in that summary — and answers `{ reverted: <turns> }`. **Files are not
|
|||||||
touched**: what the turns changed on disk stays, and the client says so beside the control. The
|
touched**: what the turns changed on disk stays, and the client says so beside the control. The
|
||||||
client then sends the new prompt as a new turn.
|
client then sends the new prompt as a new turn.
|
||||||
|
|
||||||
|
### `_lembas/show` — agent → client → result (capability `show`)
|
||||||
|
|
||||||
|
The `show` tool (spec 3.0.0), in a session the client opened, where the client said `show: true`:
|
||||||
|
the device sends the file, since the client cannot reach the device's disk. Params
|
||||||
|
`{ sessionId, toolCallId, path, kind: "markdown" | "code" | "text" | "html", language?, line?, text }`
|
||||||
|
— `path` relative to the session's working directory, `kind` from the file's name (`.md` markdown;
|
||||||
|
`.html`/`.htm` html; a known source extension code, with `language`; anything else that is text,
|
||||||
|
text), `text` the content, at most 1 MB (a larger file is refused before sending, and the model is
|
||||||
|
told). The client shows it in its viewer beside the chat — the web UI, or Desktop's — and HTML only
|
||||||
|
in a sandboxed frame that reaches neither the app nor the instance. Result `{ shown: true }` or
|
||||||
|
`{ shown: false, reason }`, which the tool answers the model with.
|
||||||
|
|
||||||
### `_lembas/session/compact` — client → agent → result
|
### `_lembas/session/compact` — client → agent → result
|
||||||
|
|
||||||
Params `{ sessionId }`. Result `{ summary }`. `-32602` while it works.
|
Params `{ sessionId }`. Result `{ summary }`. `-32602` while it works.
|
||||||
@@ -519,21 +781,27 @@ Besides `/v1/models`, the chat completions, `/api/v1/instance` and `/mcp`:
|
|||||||
as `<connection>/<served id>` when no `embedding:` is set, and never replaces one that is;
|
as `<connection>/<served id>` when no `embedding:` is set, and never replaces one that is;
|
||||||
`embedding: <served id>` also finds it. Voice uses the instance's own speech endpoints, not these
|
`embedding: <served id>` also finds it. Voice uses the instance's own speech endpoints, not these
|
||||||
entries. Any other kind is ignored.
|
entries. Any other kind is ignored.
|
||||||
|
- **`GET /api/v1/instance`** (the `models` scope) →
|
||||||
|
`{ service, version, name, connection, account: { email, name }, scope, default_model, services }`.
|
||||||
|
**`scope`** is what the token presented may do, space-separated, as its login granted it
|
||||||
|
(`models memory link`). `lembas login --token-stdin --server <url>` takes a token another
|
||||||
|
program already holds (LLeMbas Desktop: the `agent_token` of the one approval, App API) on
|
||||||
|
stdin, never from an argument or the environment, and proves it here before writing anything:
|
||||||
|
discovery must list protocol 3 (else the device login's own refusal), a 401 is a token the
|
||||||
|
instance does not accept, a 404 or an answer without `scope` an older server, and a `scope`
|
||||||
|
without `link` is refused. Then the login is stored exactly as a device login's, and `--json`
|
||||||
|
prints the device login's `done` line: `{ event: "done", fresh: true, connection, base_url,
|
||||||
|
version, models, changes }` (or `{ event: "error", message }`).
|
||||||
- **`GET /v1/usage`** →
|
- **`GET /v1/usage`** →
|
||||||
`{ plan: { name, credits_per_month } | null, balance: number | null, month: { from, tokens_in, tokens_out, cost: number | null }, device: { tokens_in, tokens_out, cost: number | null }, unit: "credits" }`.
|
`{ month: { from, tokens_in, tokens_out, cost_money: number | null, cost_by_currency: { [currency]: number } }, device: { tokens_in, tokens_out }, currency, admin, unit: "money" }`.
|
||||||
The CLI shows the balance in its status bar when `plan` is not null, and the whole of it under
|
Counting and money, no credits (spec 3.0.0): `cost_by_currency` is the month's cost in each
|
||||||
`/usage`. A 404 (an instance without it) shows nothing.
|
currency the account's use was priced in (a connection prices in its own; `{}` when nothing was
|
||||||
|
priced), and two currencies are never added together. `cost_money` is that cost when exactly one
|
||||||
Also read, every one optional (an instance without them gets credits alone):
|
currency is involved, in `currency`; with several it is `null`. `plan`, `balance`,
|
||||||
`currency` (`"EUR"` | `"USD"` | `"GBP"`), `credit_value` (money per credit), `admin` (boolean:
|
`balance_money`, `month.cost`, `device.cost` and `device.cost_money` are still sent, as `null`,
|
||||||
no limit, never refused), `balance_money`, `month.cost_money` and `device.cost_money` (numbers or
|
and `windows` as `[]`, for a 1.x client; a 3.0.0 client does not read them. The CLI shows the
|
||||||
`null`), and `windows: [{ window, limit, spent, frees_at }]` (rolling spending windows: `window`
|
month's cost (each currency's amount, `€2.00 · $0.75`) in its status bar when anything was
|
||||||
names it, `limit` and `spent` in credits, `frees_at` an ISO time or `null`). Credits are shown
|
priced, and the whole of it under `/usage`. A 404 (an instance without it) shows nothing.
|
||||||
with up to two decimals below 1,000 and whole above; beside each, its money in `currency` where
|
|
||||||
given (`€1.23`, `$1.23`, `£1.23`) — `*_money` when sent, else the credits times `credit_value`.
|
|
||||||
`admin: true` shows `unlimited` in the status bar whatever the plan, and `/usage` says so, still
|
|
||||||
listing the month's and the device's spend; its windows are not listed. Otherwise `/usage` lists
|
|
||||||
each window's spend, limit and when it frees.
|
|
||||||
- **`GET /v1/me/personalization`** →
|
- **`GET /v1/me/personalization`** →
|
||||||
`{ enabled, available, personality, personality_custom, instructions }`; **`PUT`** the same fields
|
`{ enabled, available, personality, personality_custom, instructions }`; **`PUT`** the same fields
|
||||||
(403 when personalization is not `available` **or** the token is not a device's token — the
|
(403 when personalization is not `available` **or** the token is not a device's token — the
|
||||||
@@ -545,18 +813,50 @@ Besides `/v1/models`, the chat completions, `/api/v1/instance` and `/mcp`:
|
|||||||
`enabled: true` — never the session's copy of the other two, which may be blanked or mapped — so
|
`enabled: true` — never the session's copy of the other two, which may be blanked or mapped — so
|
||||||
the instance keeps what it has saved. LLeMbas must accept a partial `PUT`. Logged out, the local
|
the instance keeps what it has saved. LLeMbas must accept a partial `PUT`. Logged out, the local
|
||||||
keys apply.
|
keys apply.
|
||||||
- **The library**: logged in to one instance whose token has the `library` scope, a config that sets
|
- **The library**: logged in to one instance whose token has the `memory` scope (or the older
|
||||||
no `library:` uses the account's (`lembas`); `library: local` written stays local.
|
`library`), a config that sets no `library:` uses the account's (`lembas`); `library: local`
|
||||||
|
written stays local. The device's login asks for `models memory link`.
|
||||||
**Scope** (capability `library`): on a `tools/call` to `/mcp` made for a session the client opened,
|
**Scope** (capability `library`): on a `tools/call` to `/mcp` made for a session the client opened,
|
||||||
the device sends `params._meta.lembas.scope: <sessionId>`. The instance then answers within that
|
the device sends `params._meta.lembas.scope: <sessionId>`. The instance then answers within that
|
||||||
chat's scope — its knowledge bases (`conformance/library_scope.json` for how `base` is read
|
chat's scope — its knowledge bases (`conformance/library_scope.json` for how `base` is read
|
||||||
against them) and its data group — and refuses (`-32602`) a session id that is not one of this
|
against them) and its data group — and refuses (`-32602`) a session id that is not one of this
|
||||||
device's. Without `scope`, the account's default data group, as before.
|
device's. Without `scope`, the account's default data group, as before.
|
||||||
|
|
||||||
|
- **Memory** (spec 3.0.0): memory records are the instance's, for the device as for a chat. Logged
|
||||||
|
in to an instance whose token has the `memory` scope, the CLI's `memory` tool and the memory it
|
||||||
|
puts into the prompt are the instance's records, read and written through `/mcp`: the tool
|
||||||
|
`memory` (the spec's, `tools/memory.json`), and the resource **`lembas://memory`**, whose text is
|
||||||
|
the entries as the memory block of a system prompt shows them (empty: none), which the device
|
||||||
|
reads as a session starts and keeps for that session (the prompt does not change under a
|
||||||
|
session). Only `memory` is taken from `/mcp`: notes, knowledge bases and skills stay the device's
|
||||||
|
own. A call made for a session the client opened carries `params._meta.lembas.scope:
|
||||||
|
<sessionId>` (on the tool call and the resource read alike) and lands in that chat's data group;
|
||||||
|
a session of the device's own lands in the data group of the session's model. A token with the
|
||||||
|
older `library` scope counts as `memory`. Local memory files are not synced up; logged out, there
|
||||||
|
is no memory.
|
||||||
|
|
||||||
|
- **Schedules** (spec 3.5.0): the schedule routes of `/api/v2` — `GET`/`POST /api/v2/schedules`,
|
||||||
|
`GET`/`PATCH`/`DELETE /api/v2/schedules/{id}`, `POST …/{id}/pause`, `/resume`, `/run`,
|
||||||
|
`GET …/{id}/runs`, `GET …/{id}/runs/{run}`, `POST /api/v2/schedules/compile` and `/preview` —
|
||||||
|
**accept the device's token** (scope `link`) as well as an app's (`app`), for the same person and
|
||||||
|
under the same `schedule.use` permission; nothing else of `/api/v2` does. The shapes are the
|
||||||
|
server's (its contract fixtures); what the CLI's `/schedule` relies on: a schedule's `id`,
|
||||||
|
`title`, `instruction`, `description`, `enabled`, `next_run`, `last_run` (`status`, `timing`,
|
||||||
|
`due_at`, `finished_at`, `preview`, `error.text`), `running`; a run's `id`, `status`, `due_at`,
|
||||||
|
`started_at`, `finished_at`, `model.name`, `preview`, `error`; compile's `ok`, `reason`, `title`,
|
||||||
|
`instruction`, `rule`, `description`, `next_runs`, `timezone`. A run's `status` may also be
|
||||||
|
**`unchanged`** (monitor mode: the watch saw nothing new; no model ran), and `waiting_for` may be
|
||||||
|
**`device`**. A schedule that runs on a linked computer has
|
||||||
|
**`runs_on: { device, device_name, cwd, mode, approval_wait_minutes }`** (else `null`); one that
|
||||||
|
watches has **`watch: { kind: "url", url } | { kind: "command", command, timeout_s }`** (else
|
||||||
|
`null`). Creating one with the device's token, `runs_on.device: "this"` names the calling device.
|
||||||
|
A server without these answers 401 or 403 to the device's token, and the CLI says the server is
|
||||||
|
older.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- `_lembas/files/search`'s `root` is the session's working directory, which `@path` mentions
|
- `_lembas/files/search`'s `root` is the session's working directory, which `@path` mentions
|
||||||
resolve against (for a session opened at the project root, the project root). `/plan` without a
|
resolve against (for a session opened at the project root, the project root). `/plan` without a
|
||||||
task is an action, with a task a prompt run in plan mode. Questions and plans also stop waiting
|
task is an action, with a task a prompt run in plan mode. Questions and plans also stop waiting
|
||||||
after the device's `remote.approval_timeout`, like approvals.
|
after the device's `remote.approval_timeout`, like approvals.
|
||||||
- The CLI's login accepts an instance whose discovery says protocol 1 or 2.
|
- The CLI's login accepts an instance whose discovery lists protocol 3 (*Versions*).
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"about": "Text → the pieces a knowledge document is cut into for search: about `size` characters (raised to 200 when smaller), cut just after a paragraph break, else a line break, else a sentence's \". \", found in the window's last third, with `overlap` characters (at most half the size) repeated; CRLF read as LF; pieces under 40 characters dropped.",
|
"about": "Text → the pieces a knowledge document is cut into for search: about `size` characters (raised to 200 when smaller), cut just after a paragraph break, else a line break, else a sentence's \". \", found in the window's last third, with `overlap` characters (at most half the size) repeated; CRLF read as LF; pieces under 40 characters dropped. Spec 3.0.0: indexing for search cuts by tokens instead (chunk_tokens.json, filesystem.md); this character splitter stays the rule wherever a piece is still measured in characters.",
|
||||||
"cases": [
|
"cases": [
|
||||||
{
|
{
|
||||||
"size": 200,
|
"size": 200,
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"about": "Text → the pieces a file is cut into for search, measured in tokens (filesystem.md, Indexing, by tokens): at most `size` tokens each (raised to 16 when smaller), the next one beginning at the start of the token `overlap` tokens (at most half the size) before the cut; the cut just after a paragraph break, else a line break, else a sentence's \". \", looked for in the window's last third (by characters), else at the end of the window's last token; CRLF read as LF, the text trimmed, every piece trimmed; pieces under 40 characters dropped; a text of at most `size` tokens is one piece. `tokenizer: words` is the stand-in for an embedder's: a token is a run of non-whitespace characters. LLeMbas: services/library/chunks.py (to be written); CLI: library/chunks.ts (to be written).",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"tokenizer": "words",
|
||||||
|
"size": 480,
|
||||||
|
"overlap": 64,
|
||||||
|
"text": "One short paragraph that fits in a single piece.",
|
||||||
|
"expect": [
|
||||||
|
"One short paragraph that fits in a single piece."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tokenizer": "words",
|
||||||
|
"size": 16,
|
||||||
|
"overlap": 4,
|
||||||
|
"text": "The first paragraph is here, with nine words in it.\n\nThe second paragraph follows it and has ten words in total.\n\nA third one closes the text with a few more words.",
|
||||||
|
"expect": [
|
||||||
|
"The first paragraph is here, with nine words in it.\n\nThe second paragraph follows it and",
|
||||||
|
"paragraph follows it and has ten words in total.\n\nA third one closes the text with"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tokenizer": "words",
|
||||||
|
"size": 20,
|
||||||
|
"overlap": 5,
|
||||||
|
"text": "Alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi omicron pi rho sigma tau upsilon phi chi psi omega. Alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi omicron pi rho sigma tau upsilon phi chi psi omega.",
|
||||||
|
"expect": [
|
||||||
|
"Alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi omicron pi rho sigma tau upsilon",
|
||||||
|
"pi rho sigma tau upsilon phi chi psi omega. Alpha beta gamma delta epsilon zeta eta theta iota kappa lambda",
|
||||||
|
"eta theta iota kappa lambda mu nu xi omicron pi rho sigma tau upsilon phi chi psi omega."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tokenizer": "words",
|
||||||
|
"size": 16,
|
||||||
|
"overlap": 4,
|
||||||
|
"text": "line one has four\nline two has four\nline three has five words\nline four has five words\nline five is the last one here\n",
|
||||||
|
"expect": [
|
||||||
|
"line one has four\nline two has four\nline three has five words",
|
||||||
|
"three has five words\nline four has five words\nline five is the last one here"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tokenizer": "words",
|
||||||
|
"size": 16,
|
||||||
|
"overlap": 40,
|
||||||
|
"text": "w0 w1 w2 w3 w4 w5 w6 w7 w8 w9 w10 w11 w12 w13 w14 w15 w16 w17 w18 w19 w20 w21 w22 w23 w24 w25 w26 w27 w28 w29 w30 w31 w32 w33 w34 w35 w36 w37 w38 w39 w40 w41 w42 w43 w44 w45 w46 w47 w48 w49 w50 w51 w52 w53 w54 w55 w56 w57 w58 w59",
|
||||||
|
"expect": [
|
||||||
|
"w0 w1 w2 w3 w4 w5 w6 w7 w8 w9 w10 w11 w12 w13 w14 w15",
|
||||||
|
"w8 w9 w10 w11 w12 w13 w14 w15 w16 w17 w18 w19 w20 w21 w22 w23",
|
||||||
|
"w16 w17 w18 w19 w20 w21 w22 w23 w24 w25 w26 w27 w28 w29 w30 w31",
|
||||||
|
"w24 w25 w26 w27 w28 w29 w30 w31 w32 w33 w34 w35 w36 w37 w38 w39",
|
||||||
|
"w32 w33 w34 w35 w36 w37 w38 w39 w40 w41 w42 w43 w44 w45 w46 w47",
|
||||||
|
"w40 w41 w42 w43 w44 w45 w46 w47 w48 w49 w50 w51 w52 w53 w54 w55",
|
||||||
|
"w48 w49 w50 w51 w52 w53 w54 w55 w56 w57 w58 w59"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tokenizer": "words",
|
||||||
|
"size": 16,
|
||||||
|
"overlap": 4,
|
||||||
|
"text": "a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd ",
|
||||||
|
"expect": [
|
||||||
|
"a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d",
|
||||||
|
"a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd a3f29c0e1b7d 9d8e7f6a5b4c 0011aabbccdd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tokenizer": "words",
|
||||||
|
"size": 16,
|
||||||
|
"overlap": 4,
|
||||||
|
"text": " \r\n ",
|
||||||
|
"expect": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,480 @@
|
|||||||
|
{
|
||||||
|
"about": "Context hygiene (context.md, loop.json `context`): what is inserted into the conversation for each step of a session, in order. Steps: `read` (`path`, the file's whole `content`, `offset`, `limit` — the read tool's body: `<n>: ` lines, the note when lines remain), `edit` / `write` (`path`, `before` — absent for a new file — and `after`), `output` (any other tool's result: `tool`, `args`, `text`), `recall` (`target`, `offset`, `limit`), `reply` (a new reply begins whose request does not carry the results inserted so far — a chat's next reply: they stay in the history a compaction covers, but no stub may name them) and `compact` (the table added after a summary, from the full reads since the previous compaction in the history; the request and the dedupe table are emptied). Each step but `reply` and `compact` carries the `id` the session gives its result; `reply` answers \"\", and so does a `compact` with no reads. `limits` overrides loop.json's `cap` for the case. The answer is the text each step inserts: a read headed by path, lines, length and the first 8 hex digits of the content's SHA-256, a stub for an unchanged re-read inside an earlier full read, a diff for an edit, a capped output with its elided line, a stub for a repeated listing or search with the same result. LLeMbas: services/context.py; CLI: session/results.ts.",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#a001",
|
||||||
|
"path": "src/a.ts",
|
||||||
|
"content": "const a = 1\nexport { a }\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#a002",
|
||||||
|
"path": "src/a.ts",
|
||||||
|
"content": "const a = 1\nexport { a }\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
"[#a001 src/a.ts lines 1-2 of 2 sha 583e86b4]\n1: const a = 1\n2: export { a }",
|
||||||
|
"[#a002 src/a.ts lines 1-2 of 2 sha 583e86b4] Unchanged since #a001, which already shows lines 1-2."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#b001",
|
||||||
|
"path": "notes.md",
|
||||||
|
"content": "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\n",
|
||||||
|
"limit": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#b002",
|
||||||
|
"path": "notes.md",
|
||||||
|
"content": "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\n",
|
||||||
|
"offset": 2,
|
||||||
|
"limit": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#b003",
|
||||||
|
"path": "notes.md",
|
||||||
|
"content": "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\n",
|
||||||
|
"offset": 4,
|
||||||
|
"limit": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#b004",
|
||||||
|
"path": "notes.md",
|
||||||
|
"content": "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\n",
|
||||||
|
"offset": 5,
|
||||||
|
"limit": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
"[#b001 notes.md lines 1-5 of 10 sha e71d970d]\n1: line 1\n2: line 2\n3: line 3\n4: line 4\n5: line 5\n\n(5 more lines — read again with offset 6)",
|
||||||
|
"[#b002 notes.md lines 2-4 of 10 sha e71d970d] Unchanged since #b001, which already shows lines 1-5.",
|
||||||
|
"[#b003 notes.md lines 4-8 of 10 sha e71d970d]\n4: line 4\n5: line 5\n6: line 6\n7: line 7\n8: line 8\n\n(2 more lines — read again with offset 9)",
|
||||||
|
"[#b004 notes.md lines 5-6 of 10 sha e71d970d] Unchanged since #b003, which already shows lines 4-8."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#c001",
|
||||||
|
"path": "a.py",
|
||||||
|
"content": "x = 1\ny = 2\nz = 3\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "edit",
|
||||||
|
"id": "#c002",
|
||||||
|
"path": "a.py",
|
||||||
|
"before": "x = 1\ny = 2\nz = 3\n",
|
||||||
|
"after": "x = 1\ny = 20\nz = 3\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#c003",
|
||||||
|
"path": "a.py",
|
||||||
|
"content": "x = 1\ny = 20\nz = 3\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#c004",
|
||||||
|
"path": "a.py",
|
||||||
|
"content": "x = 1\ny = 20\nz = 3\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
"[#c001 a.py lines 1-3 of 3 sha af198c55]\n1: x = 1\n2: y = 2\n3: z = 3",
|
||||||
|
"[#c002 a.py sha a6a8e52b]\n@@ -1,3 +1,3 @@\n x = 1\n-y = 2\n+y = 20\n z = 3",
|
||||||
|
"[#c003 a.py lines 1-3 of 3 sha a6a8e52b]\n1: x = 1\n2: y = 20\n3: z = 3",
|
||||||
|
"[#c004 a.py lines 1-3 of 3 sha a6a8e52b] Unchanged since #c003, which already shows lines 1-3."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#d001",
|
||||||
|
"path": "a.txt",
|
||||||
|
"content": "one\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#d002",
|
||||||
|
"path": "a.txt",
|
||||||
|
"content": "one\ntwo\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#d003",
|
||||||
|
"path": "b.txt",
|
||||||
|
"content": "one\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
"[#d001 a.txt lines 1-1 of 1 sha 2c8b08da]\n1: one",
|
||||||
|
"[#d002 a.txt lines 1-2 of 2 sha c3f9c8c2]\n1: one\n2: two",
|
||||||
|
"[#d003 b.txt lines 1-1 of 1 sha 2c8b08da]\n1: one"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"limits": {
|
||||||
|
"max_lines": 6,
|
||||||
|
"max_chars": 40000,
|
||||||
|
"head_lines": 2,
|
||||||
|
"head_chars": 20000,
|
||||||
|
"tail_lines": 2,
|
||||||
|
"tail_chars": 10000
|
||||||
|
},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#e001",
|
||||||
|
"tool": "bash",
|
||||||
|
"args": {
|
||||||
|
"command": "seq 10"
|
||||||
|
},
|
||||||
|
"text": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "recall",
|
||||||
|
"id": "#e002",
|
||||||
|
"target": "#e001",
|
||||||
|
"offset": 3,
|
||||||
|
"limit": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "recall",
|
||||||
|
"id": "#e003",
|
||||||
|
"target": "e001"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
"[#e001]\n1\n2\n[6 lines elided, full output #e001]\n9\n10",
|
||||||
|
"[#e002 recall #e001 lines 3-4 of 10]\n3\n4",
|
||||||
|
"[#e003 recall #e001 lines 1-10 of 10]\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#f001",
|
||||||
|
"tool": "list",
|
||||||
|
"args": {
|
||||||
|
"path": "src"
|
||||||
|
},
|
||||||
|
"text": "src/\n a.ts\n b.ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#f002",
|
||||||
|
"tool": "list",
|
||||||
|
"args": {
|
||||||
|
"path": "src"
|
||||||
|
},
|
||||||
|
"text": "src/\n a.ts\n b.ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#f003",
|
||||||
|
"tool": "list",
|
||||||
|
"args": {
|
||||||
|
"path": "src",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"text": "src/\n a.ts\n b.ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#f004",
|
||||||
|
"tool": "list",
|
||||||
|
"args": {
|
||||||
|
"path": "src"
|
||||||
|
},
|
||||||
|
"text": "src/\n a.ts\n b.ts\n c.ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#f005",
|
||||||
|
"tool": "list",
|
||||||
|
"args": {
|
||||||
|
"path": "src"
|
||||||
|
},
|
||||||
|
"text": "src/\n a.ts\n b.ts\n c.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
"[#f001]\nsrc/\n a.ts\n b.ts",
|
||||||
|
"[#f002] Same result as #f001.",
|
||||||
|
"[#f003]\nsrc/\n a.ts\n b.ts",
|
||||||
|
"[#f004]\nsrc/\n a.ts\n b.ts\n c.ts",
|
||||||
|
"[#f005] Same result as #f004."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#1001",
|
||||||
|
"tool": "bash",
|
||||||
|
"args": {
|
||||||
|
"command": "git status"
|
||||||
|
},
|
||||||
|
"text": "nothing to commit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#1002",
|
||||||
|
"tool": "bash",
|
||||||
|
"args": {
|
||||||
|
"command": "git status"
|
||||||
|
},
|
||||||
|
"text": "nothing to commit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#1003",
|
||||||
|
"tool": "grep",
|
||||||
|
"args": {
|
||||||
|
"pattern": "TODO"
|
||||||
|
},
|
||||||
|
"text": "a.ts:3: // TODO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#1004",
|
||||||
|
"tool": "grep",
|
||||||
|
"args": {
|
||||||
|
"pattern": "TODO"
|
||||||
|
},
|
||||||
|
"text": "a.ts:3: // TODO"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
"[#1001]\nnothing to commit",
|
||||||
|
"[#1002]\nnothing to commit",
|
||||||
|
"[#1003]\na.ts:3: // TODO",
|
||||||
|
"[#1004] Same result as #1003."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#2001",
|
||||||
|
"path": "a.md",
|
||||||
|
"content": "alpha\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#2002",
|
||||||
|
"path": "b.md",
|
||||||
|
"content": "beta\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#2003",
|
||||||
|
"path": "a.md",
|
||||||
|
"content": "alpha\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#2004",
|
||||||
|
"tool": "glob",
|
||||||
|
"args": {
|
||||||
|
"pattern": "*.md"
|
||||||
|
},
|
||||||
|
"text": "a.md\nb.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "compact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#2005",
|
||||||
|
"path": "a.md",
|
||||||
|
"content": "alpha\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#2006",
|
||||||
|
"tool": "glob",
|
||||||
|
"args": {
|
||||||
|
"pattern": "*.md"
|
||||||
|
},
|
||||||
|
"text": "a.md\nb.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "recall",
|
||||||
|
"id": "#2007",
|
||||||
|
"target": "#2002"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
"[#2001 a.md lines 1-1 of 1 sha b6a98d9c]\n1: alpha",
|
||||||
|
"[#2002 b.md lines 1-1 of 1 sha f2c82dec]\n1: beta",
|
||||||
|
"[#2003 a.md lines 1-1 of 1 sha b6a98d9c] Unchanged since #2001, which already shows lines 1-1.",
|
||||||
|
"[#2004]\na.md\nb.md",
|
||||||
|
"## Files read before this summary\n\nRead a file again for its content; recall shows a result by its id.\n\n| Path | Lines | sha | Result |\n|---|---|---|---|\n| a.md | 1-1 of 1 | b6a98d9c | #2001 |\n| b.md | 1-1 of 1 | f2c82dec | #2002 |",
|
||||||
|
"[#2005 a.md lines 1-1 of 1 sha b6a98d9c]\n1: alpha",
|
||||||
|
"[#2006]\na.md\nb.md",
|
||||||
|
"[#2007 recall #2002 lines 1-1 of 1]\n1: beta"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "compact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "recall",
|
||||||
|
"id": "#3001",
|
||||||
|
"target": "#ffff"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
"",
|
||||||
|
"[#3001] No stored result #ffff in this session."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "write",
|
||||||
|
"id": "#4001",
|
||||||
|
"path": "new.md",
|
||||||
|
"after": "# Title\n\nText.\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#4002",
|
||||||
|
"path": "empty.txt",
|
||||||
|
"content": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
"[#4001 new.md sha fd9dba9b] Created, 3 lines.",
|
||||||
|
"[#4002 empty.txt empty sha e3b0c442]\n(empty file)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"limits": {
|
||||||
|
"max_lines": 400,
|
||||||
|
"max_chars": 50,
|
||||||
|
"head_lines": 200,
|
||||||
|
"head_chars": 20,
|
||||||
|
"tail_lines": 100,
|
||||||
|
"tail_chars": 10
|
||||||
|
},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#5001",
|
||||||
|
"tool": "bash",
|
||||||
|
"args": {
|
||||||
|
"command": "cat min.js"
|
||||||
|
},
|
||||||
|
"text": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#5002",
|
||||||
|
"tool": "bash",
|
||||||
|
"args": {
|
||||||
|
"command": "cat log"
|
||||||
|
},
|
||||||
|
"text": "first line\nyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\nlast"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
"[#5001]\nxxxxxxxxxxxxxxxxxxxx […cut]\n[0 lines elided, full output #5001]",
|
||||||
|
"[#5002]\nfirst line\n[1 lines elided, full output #5002]\nlast"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#6001",
|
||||||
|
"path": "a.md",
|
||||||
|
"content": "alpha\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#6002",
|
||||||
|
"tool": "glob",
|
||||||
|
"args": {
|
||||||
|
"pattern": "*.md"
|
||||||
|
},
|
||||||
|
"text": "a.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "reply"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#6003",
|
||||||
|
"path": "a.md",
|
||||||
|
"content": "alpha\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "output",
|
||||||
|
"id": "#6004",
|
||||||
|
"tool": "glob",
|
||||||
|
"args": {
|
||||||
|
"pattern": "*.md"
|
||||||
|
},
|
||||||
|
"text": "a.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#6005",
|
||||||
|
"path": "a.md",
|
||||||
|
"content": "alpha\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#6006",
|
||||||
|
"path": "b.md",
|
||||||
|
"content": "beta\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "compact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "read",
|
||||||
|
"id": "#6007",
|
||||||
|
"path": "c.md",
|
||||||
|
"content": "gamma\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "reply"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "compact"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
"[#6001 a.md lines 1-1 of 1 sha b6a98d9c]\n1: alpha",
|
||||||
|
"[#6002]\na.md",
|
||||||
|
"",
|
||||||
|
"[#6003 a.md lines 1-1 of 1 sha b6a98d9c]\n1: alpha",
|
||||||
|
"[#6004]\na.md",
|
||||||
|
"[#6005 a.md lines 1-1 of 1 sha b6a98d9c] Unchanged since #6003, which already shows lines 1-1.",
|
||||||
|
"[#6006 b.md lines 1-1 of 1 sha f2c82dec]\n1: beta",
|
||||||
|
"## Files read before this summary\n\nRead a file again for its content; recall shows a result by its id.\n\n| Path | Lines | sha | Result |\n|---|---|---|---|\n| a.md | 1-1 of 1 | b6a98d9c | #6003 |\n| b.md | 1-1 of 1 | f2c82dec | #6006 |",
|
||||||
|
"[#6007 c.md lines 1-1 of 1 sha ae9a6306]\n1: gamma",
|
||||||
|
"",
|
||||||
|
"## Files read before this summary\n\nRead a file again for its content; recall shows a result by its id.\n\n| Path | Lines | sha | Result |\n|---|---|---|---|\n| c.md | 1-1 of 1 | ae9a6306 | #6007 |"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
{
|
||||||
|
"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, word for word — except where the case says `own_reason` (a hardline's, a mode's or the unattended refusal's reason is each project's), when only the answer is compared. `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"
|
||||||
|
},
|
||||||
|
"own_reason": true,
|
||||||
|
"expect": {
|
||||||
|
"answer": "refused"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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 /"
|
||||||
|
},
|
||||||
|
"own_reason": true,
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"own_reason": true,
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,876 @@
|
|||||||
|
{
|
||||||
|
"about": "The file system as one session sees it (filesystem.md): `tree` (paths; a directory ends with `/`, parents are implied), `mounts` (directories that are forge repositories), `grants` (path → level none/ro/stage/commit, covering everything below; the most specific wins; none by default), `floor` (path → the most a session may have there; the most specific applies), `cwd` (the start directory, default `/`), and one `op` on `path`: `level` (the session's level there, and whether it is a passage: at none with something granted below), `list` (the entries the session sees: directories and mounts with `/`, a mount ` [mount]`, an entry at ro in a writable directory ` [read-only]`; directories first, then by name), `read`, `write` (a file), `mkdir`, `rm`, `commit`. Answers `{ok: true}`, `{entries}`, `{level, passage}` or `{error}` with the exact text, naming the resolved absolute path. A path the session cannot see is answered by what the model already knows: by its nearest ancestor that is visible (No such file or directory) or a passage (Access denied) — so a hidden entry and a missing one read the same; creating a name taken by an entry at none is denied. LLeMbas: services/fs.py (the view) with security/access.py (levels and grants); CLI: not run until it has a view of its own (an agent's disk is one level for the whole working directory).",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "level",
|
||||||
|
"path": "/Dir1",
|
||||||
|
"expect": {
|
||||||
|
"level": "none",
|
||||||
|
"passage": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "level",
|
||||||
|
"path": "/Dir2",
|
||||||
|
"expect": {
|
||||||
|
"level": "none",
|
||||||
|
"passage": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "level",
|
||||||
|
"path": "/Dir1/Dir3/a.md",
|
||||||
|
"expect": {
|
||||||
|
"level": "stage",
|
||||||
|
"passage": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "list",
|
||||||
|
"path": "/Dir1",
|
||||||
|
"expect": {
|
||||||
|
"error": "Access denied: /Dir1 is not open to this session."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "list",
|
||||||
|
"path": "/",
|
||||||
|
"expect": {
|
||||||
|
"error": "Access denied: / is not open to this session."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "read",
|
||||||
|
"path": "/Dir1/FILE1.txt",
|
||||||
|
"expect": {
|
||||||
|
"error": "Access denied: /Dir1/FILE1.txt is not open to this session."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "write",
|
||||||
|
"path": "/Dir1/new.txt",
|
||||||
|
"expect": {
|
||||||
|
"error": "Access denied: /Dir1/new.txt is not open to this session."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "mkdir",
|
||||||
|
"path": "/Dir1/x",
|
||||||
|
"expect": {
|
||||||
|
"error": "Access denied: /Dir1/x is not open to this session."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "read",
|
||||||
|
"path": "/Dir2/b.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "Access denied: /Dir2/b.md is not open to this session."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "read",
|
||||||
|
"path": "/Dir2",
|
||||||
|
"expect": {
|
||||||
|
"error": "Access denied: /Dir2 is not open to this session."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "read",
|
||||||
|
"path": "/Nope.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "Access denied: /Nope.md is not open to this session."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "read",
|
||||||
|
"path": "/Dir1/Dir3/missing.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "No such file or directory: /Dir1/Dir3/missing.md"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "write",
|
||||||
|
"path": "/Dir2/x.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "Access denied: /Dir2/x.md is not open to this session."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "list",
|
||||||
|
"path": "/Dir1/Dir3",
|
||||||
|
"expect": {
|
||||||
|
"entries": [
|
||||||
|
"a.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "read",
|
||||||
|
"path": "/AGENTS.md",
|
||||||
|
"expect": {
|
||||||
|
"ok": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "write",
|
||||||
|
"path": "/AGENTS.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "Read-only: /AGENTS.md can be read in this session but not changed."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "write",
|
||||||
|
"path": "/Dir1/Dir3/new.md",
|
||||||
|
"expect": {
|
||||||
|
"ok": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "write",
|
||||||
|
"path": "new.md",
|
||||||
|
"cwd": "/Dir1/Dir3",
|
||||||
|
"expect": {
|
||||||
|
"ok": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "read",
|
||||||
|
"path": "../FILE1.txt",
|
||||||
|
"cwd": "/Dir1/Dir3",
|
||||||
|
"expect": {
|
||||||
|
"error": "Access denied: /Dir1/FILE1.txt is not open to this session."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/",
|
||||||
|
"/Dir1/FILE1.txt",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/",
|
||||||
|
"/Dir1/Dir3/a.md",
|
||||||
|
"/Dir2/",
|
||||||
|
"/Dir2/b.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"op": "list",
|
||||||
|
"path": "/Dir1/Dir3/../../Dir1//Dir3/",
|
||||||
|
"expect": {
|
||||||
|
"entries": [
|
||||||
|
"a.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/Notes/",
|
||||||
|
"/Notes/a.md",
|
||||||
|
"/Notes/Private/",
|
||||||
|
"/Notes/Private/p.md",
|
||||||
|
"/Ref/",
|
||||||
|
"/Ref/r.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "stage",
|
||||||
|
"/Notes/Private": "none",
|
||||||
|
"/Ref": "ro"
|
||||||
|
},
|
||||||
|
"op": "list",
|
||||||
|
"path": "/",
|
||||||
|
"expect": {
|
||||||
|
"entries": [
|
||||||
|
"Notes/",
|
||||||
|
"Ref/ [read-only]"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/Notes/",
|
||||||
|
"/Notes/a.md",
|
||||||
|
"/Notes/Private/",
|
||||||
|
"/Notes/Private/p.md",
|
||||||
|
"/Ref/",
|
||||||
|
"/Ref/r.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "stage",
|
||||||
|
"/Notes/Private": "none",
|
||||||
|
"/Ref": "ro"
|
||||||
|
},
|
||||||
|
"op": "list",
|
||||||
|
"path": "/Notes",
|
||||||
|
"expect": {
|
||||||
|
"entries": [
|
||||||
|
"a.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/Notes/",
|
||||||
|
"/Notes/a.md",
|
||||||
|
"/Notes/Private/",
|
||||||
|
"/Notes/Private/p.md",
|
||||||
|
"/Ref/",
|
||||||
|
"/Ref/r.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "stage",
|
||||||
|
"/Notes/Private": "none",
|
||||||
|
"/Ref": "ro"
|
||||||
|
},
|
||||||
|
"op": "read",
|
||||||
|
"path": "/Notes/Private/p.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "No such file or directory: /Notes/Private/p.md"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/Notes/",
|
||||||
|
"/Notes/a.md",
|
||||||
|
"/Notes/Private/",
|
||||||
|
"/Notes/Private/p.md",
|
||||||
|
"/Ref/",
|
||||||
|
"/Ref/r.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "stage",
|
||||||
|
"/Notes/Private": "none",
|
||||||
|
"/Ref": "ro"
|
||||||
|
},
|
||||||
|
"op": "mkdir",
|
||||||
|
"path": "/Notes/Private",
|
||||||
|
"expect": {
|
||||||
|
"error": "Access denied: /Notes/Private is not open to this session."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/Notes/",
|
||||||
|
"/Notes/a.md",
|
||||||
|
"/Notes/Private/",
|
||||||
|
"/Notes/Private/p.md",
|
||||||
|
"/Ref/",
|
||||||
|
"/Ref/r.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "stage",
|
||||||
|
"/Notes/Private": "none",
|
||||||
|
"/Ref": "ro"
|
||||||
|
},
|
||||||
|
"op": "write",
|
||||||
|
"path": "/Notes/Private/x.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "Access denied: /Notes/Private/x.md is not open to this session."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/Notes/",
|
||||||
|
"/Notes/a.md",
|
||||||
|
"/Notes/Private/",
|
||||||
|
"/Notes/Private/p.md",
|
||||||
|
"/Ref/",
|
||||||
|
"/Ref/r.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "stage",
|
||||||
|
"/Notes/Private": "none",
|
||||||
|
"/Ref": "ro"
|
||||||
|
},
|
||||||
|
"op": "rm",
|
||||||
|
"path": "/Notes/Private",
|
||||||
|
"expect": {
|
||||||
|
"error": "No such file or directory: /Notes/Private"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/Notes/",
|
||||||
|
"/Notes/a.md",
|
||||||
|
"/Notes/Private/",
|
||||||
|
"/Notes/Private/p.md",
|
||||||
|
"/Ref/",
|
||||||
|
"/Ref/r.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "stage",
|
||||||
|
"/Notes/Private": "none",
|
||||||
|
"/Ref": "ro"
|
||||||
|
},
|
||||||
|
"op": "rm",
|
||||||
|
"path": "/Notes/gone.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "No such file or directory: /Notes/gone.md"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/Notes/",
|
||||||
|
"/Notes/a.md",
|
||||||
|
"/Notes/Private/",
|
||||||
|
"/Notes/Private/p.md",
|
||||||
|
"/Ref/",
|
||||||
|
"/Ref/r.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "stage",
|
||||||
|
"/Notes/Private": "none",
|
||||||
|
"/Ref": "ro"
|
||||||
|
},
|
||||||
|
"op": "write",
|
||||||
|
"path": "/Ref/r.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "Read-only: /Ref/r.md can be read in this session but not changed."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/Notes/",
|
||||||
|
"/Notes/a.md",
|
||||||
|
"/Notes/Private/",
|
||||||
|
"/Notes/Private/p.md",
|
||||||
|
"/Ref/",
|
||||||
|
"/Ref/r.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "stage",
|
||||||
|
"/Notes/Private": "none",
|
||||||
|
"/Ref": "ro"
|
||||||
|
},
|
||||||
|
"op": "write",
|
||||||
|
"path": "/Ref/new.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "Read-only: /Ref/new.md can be read in this session but not changed."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/Notes/",
|
||||||
|
"/Notes/a.md",
|
||||||
|
"/Notes/Private/",
|
||||||
|
"/Notes/Private/p.md",
|
||||||
|
"/Ref/",
|
||||||
|
"/Ref/r.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "stage",
|
||||||
|
"/Notes/Private": "none",
|
||||||
|
"/Ref": "ro"
|
||||||
|
},
|
||||||
|
"op": "rm",
|
||||||
|
"path": "/Notes/a.md",
|
||||||
|
"expect": {
|
||||||
|
"ok": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/Notes/",
|
||||||
|
"/Notes/a.md",
|
||||||
|
"/Notes/Private/",
|
||||||
|
"/Notes/Private/p.md",
|
||||||
|
"/Ref/",
|
||||||
|
"/Ref/r.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "commit"
|
||||||
|
},
|
||||||
|
"floor": {
|
||||||
|
"/Ref": "ro"
|
||||||
|
},
|
||||||
|
"op": "level",
|
||||||
|
"path": "/Ref/r.md",
|
||||||
|
"expect": {
|
||||||
|
"level": "ro",
|
||||||
|
"passage": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/Notes/",
|
||||||
|
"/Notes/a.md",
|
||||||
|
"/Notes/Private/",
|
||||||
|
"/Notes/Private/p.md",
|
||||||
|
"/Ref/",
|
||||||
|
"/Ref/r.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "commit"
|
||||||
|
},
|
||||||
|
"floor": {
|
||||||
|
"/Ref": "ro"
|
||||||
|
},
|
||||||
|
"op": "write",
|
||||||
|
"path": "/Ref/r.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "Read-only: /Ref/r.md can be read in this session but not changed."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/repo/",
|
||||||
|
"/repo/README.md",
|
||||||
|
"/repo/src/",
|
||||||
|
"/repo/src/x.ts",
|
||||||
|
"/home/",
|
||||||
|
"/home/n.md"
|
||||||
|
],
|
||||||
|
"mounts": [
|
||||||
|
"/repo"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "commit",
|
||||||
|
"/repo": "stage"
|
||||||
|
},
|
||||||
|
"op": "list",
|
||||||
|
"path": "/",
|
||||||
|
"expect": {
|
||||||
|
"entries": [
|
||||||
|
"home/",
|
||||||
|
"repo/ [mount]"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/repo/",
|
||||||
|
"/repo/README.md",
|
||||||
|
"/repo/src/",
|
||||||
|
"/repo/src/x.ts",
|
||||||
|
"/home/",
|
||||||
|
"/home/n.md"
|
||||||
|
],
|
||||||
|
"mounts": [
|
||||||
|
"/repo"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "commit",
|
||||||
|
"/repo": "stage"
|
||||||
|
},
|
||||||
|
"op": "commit",
|
||||||
|
"path": "/repo/src/x.ts",
|
||||||
|
"expect": {
|
||||||
|
"error": "Not committed: this session may change /repo/src/x.ts but not commit it; the change stays staged."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/repo/",
|
||||||
|
"/repo/README.md",
|
||||||
|
"/repo/src/",
|
||||||
|
"/repo/src/x.ts",
|
||||||
|
"/home/",
|
||||||
|
"/home/n.md"
|
||||||
|
],
|
||||||
|
"mounts": [
|
||||||
|
"/repo"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "commit"
|
||||||
|
},
|
||||||
|
"op": "commit",
|
||||||
|
"path": "/home/n.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "Nothing to commit: /home/n.md is not in a repository."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/repo/",
|
||||||
|
"/repo/README.md",
|
||||||
|
"/repo/src/",
|
||||||
|
"/repo/src/x.ts",
|
||||||
|
"/home/",
|
||||||
|
"/home/n.md"
|
||||||
|
],
|
||||||
|
"mounts": [
|
||||||
|
"/repo"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/repo": "commit"
|
||||||
|
},
|
||||||
|
"op": "commit",
|
||||||
|
"path": "/repo/README.md",
|
||||||
|
"expect": {
|
||||||
|
"ok": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/repo/",
|
||||||
|
"/repo/README.md",
|
||||||
|
"/repo/src/",
|
||||||
|
"/repo/src/x.ts",
|
||||||
|
"/home/",
|
||||||
|
"/home/n.md"
|
||||||
|
],
|
||||||
|
"mounts": [
|
||||||
|
"/repo"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/repo": "ro"
|
||||||
|
},
|
||||||
|
"op": "commit",
|
||||||
|
"path": "/repo/README.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "Read-only: /repo/README.md can be read in this session but not changed."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/repo/",
|
||||||
|
"/repo/README.md",
|
||||||
|
"/repo/src/",
|
||||||
|
"/repo/src/x.ts",
|
||||||
|
"/home/",
|
||||||
|
"/home/n.md"
|
||||||
|
],
|
||||||
|
"mounts": [
|
||||||
|
"/repo"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "commit"
|
||||||
|
},
|
||||||
|
"op": "write",
|
||||||
|
"path": "/repo/src",
|
||||||
|
"expect": {
|
||||||
|
"error": "Is a directory: /repo/src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/repo/",
|
||||||
|
"/repo/README.md",
|
||||||
|
"/repo/src/",
|
||||||
|
"/repo/src/x.ts",
|
||||||
|
"/home/",
|
||||||
|
"/home/n.md"
|
||||||
|
],
|
||||||
|
"mounts": [
|
||||||
|
"/repo"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "commit"
|
||||||
|
},
|
||||||
|
"op": "write",
|
||||||
|
"path": "/home/n.md/x",
|
||||||
|
"expect": {
|
||||||
|
"error": "Not a directory: /home/n.md"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/repo/",
|
||||||
|
"/repo/README.md",
|
||||||
|
"/repo/src/",
|
||||||
|
"/repo/src/x.ts",
|
||||||
|
"/home/",
|
||||||
|
"/home/n.md"
|
||||||
|
],
|
||||||
|
"mounts": [
|
||||||
|
"/repo"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "commit"
|
||||||
|
},
|
||||||
|
"op": "mkdir",
|
||||||
|
"path": "/home/n.md",
|
||||||
|
"expect": {
|
||||||
|
"error": "Already exists: /home/n.md"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
"/repo/",
|
||||||
|
"/repo/README.md",
|
||||||
|
"/repo/src/",
|
||||||
|
"/repo/src/x.ts",
|
||||||
|
"/home/",
|
||||||
|
"/home/n.md"
|
||||||
|
],
|
||||||
|
"mounts": [
|
||||||
|
"/repo"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "commit"
|
||||||
|
},
|
||||||
|
"op": "mkdir",
|
||||||
|
"path": "/home",
|
||||||
|
"expect": {
|
||||||
|
"ok": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"about": "The instruction files a session starts with (filesystem.md, Instruction files, stacked): `files` that exist, the `boundary` and the `start` directory (outside the boundary: read as the boundary), and `grants` as in fs_access (absent: everything readable). From the boundary down to the start directory, outermost first, each directory gives the first of AGENTS.md, CLAUDE.md that is a file the session can read — never both; a file at none (in a passage, or taken away) is not loaded and the walk goes on below it. The answer is the loaded paths, in order. The CLI's global AGENTS.md, before the chain, and its configured `instructions`, after it, are not part of these cases. LLeMbas: services/workspace.py chain; CLI: prompt/assemble.ts instructionChain.",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/sec/AGENTS.md",
|
||||||
|
"/sec/repo/CLAUDE.md",
|
||||||
|
"/sec/repo/src/AGENTS.md"
|
||||||
|
],
|
||||||
|
"boundary": "/",
|
||||||
|
"start": "/sec/repo",
|
||||||
|
"expect": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/sec/AGENTS.md",
|
||||||
|
"/sec/repo/CLAUDE.md"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/sec/AGENTS.md",
|
||||||
|
"/sec/repo/CLAUDE.md",
|
||||||
|
"/sec/repo/src/AGENTS.md"
|
||||||
|
],
|
||||||
|
"boundary": "/",
|
||||||
|
"start": "/sec/repo/src",
|
||||||
|
"expect": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/sec/AGENTS.md",
|
||||||
|
"/sec/repo/CLAUDE.md",
|
||||||
|
"/sec/repo/src/AGENTS.md"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/sec/AGENTS.md",
|
||||||
|
"/sec/repo/CLAUDE.md",
|
||||||
|
"/sec/repo/src/AGENTS.md"
|
||||||
|
],
|
||||||
|
"boundary": "/sec",
|
||||||
|
"start": "/sec/repo",
|
||||||
|
"expect": [
|
||||||
|
"/sec/AGENTS.md",
|
||||||
|
"/sec/repo/CLAUDE.md"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/sec/AGENTS.md",
|
||||||
|
"/sec/repo/CLAUDE.md",
|
||||||
|
"/sec/repo/src/AGENTS.md"
|
||||||
|
],
|
||||||
|
"boundary": "/sec",
|
||||||
|
"start": "/other",
|
||||||
|
"expect": [
|
||||||
|
"/sec/AGENTS.md"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"/x/AGENTS.md",
|
||||||
|
"/x/CLAUDE.md"
|
||||||
|
],
|
||||||
|
"boundary": "/",
|
||||||
|
"start": "/x",
|
||||||
|
"expect": [
|
||||||
|
"/x/AGENTS.md"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/AGENTS.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/AGENTS.md": "ro",
|
||||||
|
"/Dir1/Dir3": "stage"
|
||||||
|
},
|
||||||
|
"boundary": "/",
|
||||||
|
"start": "/Dir1/Dir3",
|
||||||
|
"expect": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/Dir1/Dir3/AGENTS.md"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"/x/AGENTS.md",
|
||||||
|
"/x/CLAUDE.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/": "ro",
|
||||||
|
"/x/AGENTS.md": "none"
|
||||||
|
},
|
||||||
|
"boundary": "/",
|
||||||
|
"start": "/x",
|
||||||
|
"expect": [
|
||||||
|
"/x/CLAUDE.md"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"/AGENTS.md",
|
||||||
|
"/a/AGENTS.md",
|
||||||
|
"/a/b/AGENTS.md"
|
||||||
|
],
|
||||||
|
"grants": {
|
||||||
|
"/a/b": "ro"
|
||||||
|
},
|
||||||
|
"boundary": "/",
|
||||||
|
"start": "/a/b",
|
||||||
|
"expect": [
|
||||||
|
"/a/b/AGENTS.md"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"/README.md"
|
||||||
|
],
|
||||||
|
"boundary": "/",
|
||||||
|
"start": "/",
|
||||||
|
"expect": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
{
|
||||||
|
"about": "The effort levels a chat offers for a model, and what each sends (models/schema.json `levels`, spec 3.2.0). `op: levels` — `model` is model metadata (`thinking_switch`, `efforts` as given — never absent here, since an absent list is each project's own fallback — `effort`); the answer is `levels` (in offering order), `control` (`none`: one level, no effort control; `switch`: two levels, one of them off — a Thinking on/off switch; `slider`: anything else, a slider or a list) and `start` (the level a new conversation starts with: `effort` when it is one of the levels, else `medium` when offered, else the first effort, else `default`). `op: request` — `level` is the level asked for; the answer is what an OpenAI chat request body carries for it: `reasoning_effort` and `chat_template_kwargs` (only the keys set; `{}` when nothing). A level the model does not offer is read as the start level. LLeMbas: services/generation.py; CLI: provider/effort.ts.",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"about": "A model with the switch and one effort: a Thinking switch, on at its effort.",
|
||||||
|
"op": "levels",
|
||||||
|
"model": {
|
||||||
|
"thinking_switch": true,
|
||||||
|
"efforts": [
|
||||||
|
"medium"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"levels": [
|
||||||
|
"off",
|
||||||
|
"medium"
|
||||||
|
],
|
||||||
|
"control": "switch",
|
||||||
|
"start": "medium"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "The same effort without the switch: one level, no control at all.",
|
||||||
|
"op": "levels",
|
||||||
|
"model": {
|
||||||
|
"efforts": [
|
||||||
|
"medium"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"levels": [
|
||||||
|
"medium"
|
||||||
|
],
|
||||||
|
"control": "none",
|
||||||
|
"start": "medium"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "The switch and no efforts: off, or the model's own default — still a switch.",
|
||||||
|
"op": "levels",
|
||||||
|
"model": {
|
||||||
|
"thinking_switch": true,
|
||||||
|
"efforts": []
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"levels": [
|
||||||
|
"off",
|
||||||
|
"default"
|
||||||
|
],
|
||||||
|
"control": "switch",
|
||||||
|
"start": "default"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Neither: one level, the model's own; nothing is shown and nothing sent.",
|
||||||
|
"op": "levels",
|
||||||
|
"model": {
|
||||||
|
"efforts": []
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"levels": [
|
||||||
|
"default"
|
||||||
|
],
|
||||||
|
"control": "none",
|
||||||
|
"start": "default"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "The switch and three efforts: a slider, off first; medium is where it starts when nothing is said.",
|
||||||
|
"op": "levels",
|
||||||
|
"model": {
|
||||||
|
"thinking_switch": true,
|
||||||
|
"efforts": [
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"levels": [
|
||||||
|
"off",
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high"
|
||||||
|
],
|
||||||
|
"control": "slider",
|
||||||
|
"start": "medium"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Three efforts without the switch: a slider without off.",
|
||||||
|
"op": "levels",
|
||||||
|
"model": {
|
||||||
|
"efforts": [
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"levels": [
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high"
|
||||||
|
],
|
||||||
|
"control": "slider",
|
||||||
|
"start": "medium"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "The model's own start level, when it offers it.",
|
||||||
|
"op": "levels",
|
||||||
|
"model": {
|
||||||
|
"thinking_switch": true,
|
||||||
|
"efforts": [
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high"
|
||||||
|
],
|
||||||
|
"effort": "high"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"levels": [
|
||||||
|
"off",
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high"
|
||||||
|
],
|
||||||
|
"control": "slider",
|
||||||
|
"start": "high"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A start of off, with the switch: the conversation starts with thinking off.",
|
||||||
|
"op": "levels",
|
||||||
|
"model": {
|
||||||
|
"thinking_switch": true,
|
||||||
|
"efforts": [
|
||||||
|
"medium"
|
||||||
|
],
|
||||||
|
"effort": "off"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"levels": [
|
||||||
|
"off",
|
||||||
|
"medium"
|
||||||
|
],
|
||||||
|
"control": "switch",
|
||||||
|
"start": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A start of off without the switch is not a level the model offers: the start rule applies (no medium: the first effort).",
|
||||||
|
"op": "levels",
|
||||||
|
"model": {
|
||||||
|
"efforts": [
|
||||||
|
"low",
|
||||||
|
"high"
|
||||||
|
],
|
||||||
|
"effort": "off"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"levels": [
|
||||||
|
"low",
|
||||||
|
"high"
|
||||||
|
],
|
||||||
|
"control": "slider",
|
||||||
|
"start": "low"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "thinking_switch false is the same as absent.",
|
||||||
|
"op": "levels",
|
||||||
|
"model": {
|
||||||
|
"thinking_switch": false,
|
||||||
|
"efforts": [
|
||||||
|
"high",
|
||||||
|
"xhigh"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"levels": [
|
||||||
|
"high",
|
||||||
|
"xhigh"
|
||||||
|
],
|
||||||
|
"control": "slider",
|
||||||
|
"start": "high"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Two efforts and no switch: two levels, neither off — a slider, not a switch.",
|
||||||
|
"op": "levels",
|
||||||
|
"model": {
|
||||||
|
"efforts": [
|
||||||
|
"low",
|
||||||
|
"medium"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"levels": [
|
||||||
|
"low",
|
||||||
|
"medium"
|
||||||
|
],
|
||||||
|
"control": "slider",
|
||||||
|
"start": "medium"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "off sends enable_thinking false and no effort anywhere.",
|
||||||
|
"op": "request",
|
||||||
|
"model": {
|
||||||
|
"thinking_switch": true,
|
||||||
|
"efforts": [
|
||||||
|
"medium"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"level": "off",
|
||||||
|
"expect": {
|
||||||
|
"chat_template_kwargs": {
|
||||||
|
"enable_thinking": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "An effort is sent where effort_style puts it (both, by default), and enable_thinking is not sent.",
|
||||||
|
"op": "request",
|
||||||
|
"model": {
|
||||||
|
"thinking_switch": true,
|
||||||
|
"efforts": [
|
||||||
|
"medium"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"level": "medium",
|
||||||
|
"expect": {
|
||||||
|
"reasoning_effort": "medium",
|
||||||
|
"chat_template_kwargs": {
|
||||||
|
"reasoning_effort": "medium"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "effort_style top: the effort at the top level only.",
|
||||||
|
"op": "request",
|
||||||
|
"model": {
|
||||||
|
"thinking_switch": true,
|
||||||
|
"efforts": [
|
||||||
|
"low",
|
||||||
|
"medium"
|
||||||
|
],
|
||||||
|
"effort_style": "top"
|
||||||
|
},
|
||||||
|
"level": "low",
|
||||||
|
"expect": {
|
||||||
|
"reasoning_effort": "low"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "effort_style kwargs: inside chat_template_kwargs only.",
|
||||||
|
"op": "request",
|
||||||
|
"model": {
|
||||||
|
"efforts": [
|
||||||
|
"low",
|
||||||
|
"medium"
|
||||||
|
],
|
||||||
|
"effort_style": "kwargs"
|
||||||
|
},
|
||||||
|
"level": "medium",
|
||||||
|
"expect": {
|
||||||
|
"chat_template_kwargs": {
|
||||||
|
"reasoning_effort": "medium"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "default sends nothing: the model's own.",
|
||||||
|
"op": "request",
|
||||||
|
"model": {
|
||||||
|
"thinking_switch": true,
|
||||||
|
"efforts": []
|
||||||
|
},
|
||||||
|
"level": "default",
|
||||||
|
"expect": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "off, effort_style top: still only enable_thinking, in chat_template_kwargs (the only place a template reads it).",
|
||||||
|
"op": "request",
|
||||||
|
"model": {
|
||||||
|
"thinking_switch": true,
|
||||||
|
"efforts": [
|
||||||
|
"medium"
|
||||||
|
],
|
||||||
|
"effort_style": "top"
|
||||||
|
},
|
||||||
|
"level": "off",
|
||||||
|
"expect": {
|
||||||
|
"chat_template_kwargs": {
|
||||||
|
"enable_thinking": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "off asked of a model without the switch is read as its start level.",
|
||||||
|
"op": "request",
|
||||||
|
"model": {
|
||||||
|
"efforts": [
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"level": "off",
|
||||||
|
"expect": {
|
||||||
|
"reasoning_effort": "medium",
|
||||||
|
"chat_template_kwargs": {
|
||||||
|
"reasoning_effort": "medium"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A level the model does not take is read as its start level too.",
|
||||||
|
"op": "request",
|
||||||
|
"model": {
|
||||||
|
"thinking_switch": true,
|
||||||
|
"efforts": [
|
||||||
|
"medium"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"level": "high",
|
||||||
|
"expect": {
|
||||||
|
"reasoning_effort": "medium",
|
||||||
|
"chat_template_kwargs": {
|
||||||
|
"reasoning_effort": "medium"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
{
|
||||||
|
"about": "Spec 3.5.0 (acp.md *Monitor mode*, `_lembas/monitor`): the watch command on a device. `hash`: `exit` and `output` (the bytes as UTF-8) → the answer's `hash`. `judge`: a command line in the schedule's mode, with the default rules (and any extra global rules given), the project root /project and nothing named on disk, as conformance/permission.json judges it → `run` (the mode allows it without asking), or `{refused}` with `asks` (the mode would ask: scheduled.texts.monitor_asks) or `denied` (scheduled.texts.monitor_denied). The device's own; the server keeps only the hash.",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"about": "Exit 0 and a line of output.",
|
||||||
|
"kind": "hash",
|
||||||
|
"exit": 0,
|
||||||
|
"output": "ok\n",
|
||||||
|
"expect": "sha256:9427ae13c91f98a3af2213b34fb59e4bb5aa9e6101a1f7407f526dde3571e6aa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "The same output with another exit status is a change.",
|
||||||
|
"kind": "hash",
|
||||||
|
"exit": 1,
|
||||||
|
"output": "ok\n",
|
||||||
|
"expect": "sha256:21e0bbd37d04094ef29f22d0212bb0f4bedda1b1fab38d51f9b8670b5cf07df4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "No output.",
|
||||||
|
"kind": "hash",
|
||||||
|
"exit": 0,
|
||||||
|
"output": "",
|
||||||
|
"expect": "sha256:c7757c0896cbfe6182d8ea2bda4a8bf94addc428980eedab8609c57ca7ff1763"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A trailing newline counts: nothing is normalised.",
|
||||||
|
"kind": "hash",
|
||||||
|
"exit": 0,
|
||||||
|
"output": "ok",
|
||||||
|
"expect": "sha256:b9b8913ecae1ba8190ef1a22cc57afef08ad3933ab4b13a32f8cdccb07bb527b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "UTF-8 output, hashed as its bytes.",
|
||||||
|
"kind": "hash",
|
||||||
|
"exit": 0,
|
||||||
|
"output": "lembas · ✓\n",
|
||||||
|
"expect": "sha256:3d1c38d3b96ee10d71faac35eb2933ba13b68d6325872fdda3e459c9fbd17d98"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A read-only command in Edit runs.",
|
||||||
|
"kind": "judge",
|
||||||
|
"line": "git status",
|
||||||
|
"mode": "edit",
|
||||||
|
"expect": "run"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A command Edit would ask about is refused: a watch has nobody to ask.",
|
||||||
|
"kind": "judge",
|
||||||
|
"line": "rm -rf build",
|
||||||
|
"mode": "edit",
|
||||||
|
"expect": {
|
||||||
|
"refused": "asks"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "In Auto it runs.",
|
||||||
|
"kind": "judge",
|
||||||
|
"line": "rm -rf build",
|
||||||
|
"mode": "auto",
|
||||||
|
"expect": "run"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "The hardline is a floor in every mode.",
|
||||||
|
"kind": "judge",
|
||||||
|
"line": "rm -rf /",
|
||||||
|
"mode": "auto",
|
||||||
|
"expect": {
|
||||||
|
"refused": "denied"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A line that pipes a download into a shell asks in Manual: refused.",
|
||||||
|
"kind": "judge",
|
||||||
|
"line": "curl -s https://example.org/feed | sh",
|
||||||
|
"mode": "manual",
|
||||||
|
"expect": {
|
||||||
|
"refused": "asks"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A rule the person wrote allows it.",
|
||||||
|
"kind": "judge",
|
||||||
|
"line": "make check",
|
||||||
|
"mode": "edit",
|
||||||
|
"rules": {
|
||||||
|
"bash": {
|
||||||
|
"make check": "allow"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": "run"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A rule the person wrote denies it, even in Auto.",
|
||||||
|
"kind": "judge",
|
||||||
|
"line": "make deploy",
|
||||||
|
"mode": "auto",
|
||||||
|
"rules": {
|
||||||
|
"bash": {
|
||||||
|
"make deploy": "deny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "denied"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Reading a secret asks: refused.",
|
||||||
|
"kind": "judge",
|
||||||
|
"line": "cat .env",
|
||||||
|
"mode": "edit",
|
||||||
|
"expect": {
|
||||||
|
"refused": "asks"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "An ordinary listing runs in Manual.",
|
||||||
|
"kind": "judge",
|
||||||
|
"line": "ls -la",
|
||||||
|
"mode": "manual",
|
||||||
|
"expect": "run"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"about": "Where an agent session works, as the model is told it and as its tools read a path (context.md §11, spec 3.2.0). `op: env` — `root` is the project root and `started` the directory the session was started in (both absolute); the answer is the environment block's lines about them, in order, from loop.json context.env — none naming an absolute path. `op: resolve` — `path` as a tool call gives it; the answer is the absolute path it means: relative paths against the project root, whatever `started` is, `.` and `..` by their text (filesystem.md, Paths); an absolute path as it is. LLeMbas: not run — a chat's paths are the server's tree's; CLI: prompt/assemble.ts, tool/tool.ts.",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"about": "Started at the root: the project's name and nothing else.",
|
||||||
|
"op": "env",
|
||||||
|
"root": "/home/ada/src/app",
|
||||||
|
"started": "/home/ada/src/app",
|
||||||
|
"expect": [
|
||||||
|
"Project: app (paths are relative to its root)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Started below the root: that directory, relative to the root.",
|
||||||
|
"op": "env",
|
||||||
|
"root": "/home/ada/src/app",
|
||||||
|
"started": "/home/ada/src/app/packages/web",
|
||||||
|
"expect": [
|
||||||
|
"Project: app (paths are relative to its root)",
|
||||||
|
"Started in: packages/web (relative to the project root)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A trailing slash changes nothing.",
|
||||||
|
"op": "env",
|
||||||
|
"root": "/srv/work/site/",
|
||||||
|
"started": "/srv/work/site",
|
||||||
|
"expect": [
|
||||||
|
"Project: site (paths are relative to its root)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A relative path is resolved against the project root.",
|
||||||
|
"op": "resolve",
|
||||||
|
"root": "/home/ada/src/app",
|
||||||
|
"started": "/home/ada/src/app",
|
||||||
|
"path": "src/main.ts",
|
||||||
|
"expect": "/home/ada/src/app/src/main.ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "…also when the session started below the root: one base for every relative path.",
|
||||||
|
"op": "resolve",
|
||||||
|
"root": "/home/ada/src/app",
|
||||||
|
"started": "/home/ada/src/app/packages/web",
|
||||||
|
"path": "src/main.ts",
|
||||||
|
"expect": "/home/ada/src/app/src/main.ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "The directory the session started in is reached by its path from the root.",
|
||||||
|
"op": "resolve",
|
||||||
|
"root": "/home/ada/src/app",
|
||||||
|
"started": "/home/ada/src/app/packages/web",
|
||||||
|
"path": "packages/web/index.ts",
|
||||||
|
"expect": "/home/ada/src/app/packages/web/index.ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "An absolute path is still accepted, as it is.",
|
||||||
|
"op": "resolve",
|
||||||
|
"root": "/home/ada/src/app",
|
||||||
|
"started": "/home/ada/src/app/packages/web",
|
||||||
|
"path": "/etc/hosts",
|
||||||
|
"expect": "/etc/hosts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": ". is the root.",
|
||||||
|
"op": "resolve",
|
||||||
|
"root": "/home/ada/src/app",
|
||||||
|
"started": "/home/ada/src/app/packages/web",
|
||||||
|
"path": ".",
|
||||||
|
"expect": "/home/ada/src/app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": ".. is resolved by its text (whether it may be reached is the permission rules' business).",
|
||||||
|
"op": "resolve",
|
||||||
|
"root": "/home/ada/src/app",
|
||||||
|
"started": "/home/ada/src/app",
|
||||||
|
"path": "../lib/x.ts",
|
||||||
|
"expect": "/home/ada/src/lib/x.ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "./ and repeated slashes are resolved by their text.",
|
||||||
|
"op": "resolve",
|
||||||
|
"root": "/home/ada/src/app",
|
||||||
|
"started": "/home/ada/src/app",
|
||||||
|
"path": "./docs//guide.md",
|
||||||
|
"expect": "/home/ada/src/app/docs/guide.md"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,595 @@
|
|||||||
|
{
|
||||||
|
"about": "Spec 3.6.0 (phone.md §2-§3): `check` — a phone_action call's arguments, and either the action the phone is sent (`action`: its own fields only, strings trimmed, `summary` the action's name when empty) or the call's refusal (`refused`, loop.json phone.texts); `result` — the phone's answer (`status`, `message`; `status` null is no answer in time) as the text the model is told. LLeMbas only: the CLI has no runner for this area.",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"about": "A known action, its fields only: a stray field is dropped and strings are trimmed.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "alarm",
|
||||||
|
"summary": " Alarm at 7:00 on weekdays ",
|
||||||
|
"time": "07:00",
|
||||||
|
"days": [
|
||||||
|
"mon",
|
||||||
|
"tue",
|
||||||
|
"wed",
|
||||||
|
"thu",
|
||||||
|
"fri"
|
||||||
|
],
|
||||||
|
"label": " Work ",
|
||||||
|
"seconds": 60
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"action": {
|
||||||
|
"action": "alarm",
|
||||||
|
"summary": "Alarm at 7:00 on weekdays",
|
||||||
|
"time": "07:00",
|
||||||
|
"days": [
|
||||||
|
"mon",
|
||||||
|
"tue",
|
||||||
|
"wed",
|
||||||
|
"thu",
|
||||||
|
"fri"
|
||||||
|
],
|
||||||
|
"label": "Work"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "No summary: the action's name stands in.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "torch",
|
||||||
|
"on": false
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"action": {
|
||||||
|
"action": "torch",
|
||||||
|
"summary": "torch",
|
||||||
|
"on": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "An action that is not one.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "call",
|
||||||
|
"summary": "Call mum",
|
||||||
|
"number": "+421 900 123 456"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "\"call\" is not an action. Use one of: alarm, timer, calendar_event, sms_draft, email_draft, dial, navigate, add_stop, search_nearby, stop_navigation, open_app, open_url, media, torch, settings_panel, automation_intent. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A required field missing.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "timer",
|
||||||
|
"summary": "Tea"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "timer needs seconds. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Both of add_stop's missing: all of them, in order, in one sentence.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "add_stop",
|
||||||
|
"summary": "Add a stop"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "add_stop needs stop and destination. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A group: an SMS draft needs a recipient or a text.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "sms_draft",
|
||||||
|
"summary": "Text"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "sms_draft needs to or body. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A group met by one of its fields.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "sms_draft",
|
||||||
|
"summary": "Text Jana",
|
||||||
|
"to": [
|
||||||
|
"Jana"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"action": {
|
||||||
|
"action": "sms_draft",
|
||||||
|
"summary": "Text Jana",
|
||||||
|
"to": [
|
||||||
|
"Jana"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "An empty string is missing, whitespace included.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "open_app",
|
||||||
|
"summary": "Open",
|
||||||
|
"app": " "
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "open_app needs app. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A time out of range.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "alarm",
|
||||||
|
"summary": "Alarm",
|
||||||
|
"time": "24:00"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "time is not a time as HH:MM (24-hour). Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A timer of zero seconds.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "timer",
|
||||||
|
"summary": "Timer",
|
||||||
|
"seconds": 0
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "seconds is not a whole number of seconds from 1 to 86400. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A boolean is not a number of seconds.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "timer",
|
||||||
|
"summary": "Timer",
|
||||||
|
"seconds": true
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "seconds is not a whole number of seconds from 1 to 86400. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A calendar event with a date for a whole day.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "calendar_event",
|
||||||
|
"summary": "Holiday",
|
||||||
|
"title": "Holiday",
|
||||||
|
"start": "2026-12-24",
|
||||||
|
"all_day": true
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"action": {
|
||||||
|
"action": "calendar_event",
|
||||||
|
"summary": "Holiday",
|
||||||
|
"title": "Holiday",
|
||||||
|
"start": "2026-12-24",
|
||||||
|
"all_day": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A start that is not a date.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "calendar_event",
|
||||||
|
"summary": "Meet",
|
||||||
|
"title": "Meet",
|
||||||
|
"start": "tomorrow at 3"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "start is not a date and time as YYYY-MM-DDTHH:MM, or a date as YYYY-MM-DD for a whole day. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A number with too few digits.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "dial",
|
||||||
|
"summary": "Dial",
|
||||||
|
"number": "12"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "number is not a phone number. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A number with letters in it.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "dial",
|
||||||
|
"summary": "Dial",
|
||||||
|
"number": "0900 ABC"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "number is not a phone number. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A web address that is not http or https.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "open_url",
|
||||||
|
"summary": "Open",
|
||||||
|
"url": "javascript:alert(1)"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "url is not an http or https address. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A web address in capitals is still one.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "open_url",
|
||||||
|
"summary": "Open",
|
||||||
|
"url": "HTTPS://example.org/"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"action": {
|
||||||
|
"action": "open_url",
|
||||||
|
"summary": "Open",
|
||||||
|
"url": "HTTPS://example.org/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A route: a destination, stops in order, a mode.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "navigate",
|
||||||
|
"summary": "Drive home via the shop",
|
||||||
|
"destination": {
|
||||||
|
"name": "Home",
|
||||||
|
"query": "1 Bagshot Row, Hobbiton"
|
||||||
|
},
|
||||||
|
"stops": [
|
||||||
|
{
|
||||||
|
"name": "Shop",
|
||||||
|
"lat": 48.89,
|
||||||
|
"lon": 18.04
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mode": "drive"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"action": {
|
||||||
|
"action": "navigate",
|
||||||
|
"summary": "Drive home via the shop",
|
||||||
|
"destination": {
|
||||||
|
"name": "Home",
|
||||||
|
"query": "1 Bagshot Row, Hobbiton"
|
||||||
|
},
|
||||||
|
"stops": [
|
||||||
|
{
|
||||||
|
"name": "Shop",
|
||||||
|
"lat": 48.89,
|
||||||
|
"lon": 18.04
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mode": "drive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A destination with coordinates out of range and nothing else is not a place.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "navigate",
|
||||||
|
"summary": "Go",
|
||||||
|
"destination": {
|
||||||
|
"lat": 95,
|
||||||
|
"lon": 18
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "destination is not a place: lat and lon together, or a query, or a name. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A stop that is not a place, inside the list.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "navigate",
|
||||||
|
"summary": "Go",
|
||||||
|
"destination": {
|
||||||
|
"name": "Home",
|
||||||
|
"query": "1 Bagshot Row, Hobbiton"
|
||||||
|
},
|
||||||
|
"stops": [
|
||||||
|
{
|
||||||
|
"name": "Shop"
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "stops is not a list of places, each lat and lon together, or a query, or a name. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "add_stop with its destination, as the person told it.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "add_stop",
|
||||||
|
"summary": "Add the petrol station",
|
||||||
|
"stop": {
|
||||||
|
"query": "Fuel station, Bywater"
|
||||||
|
},
|
||||||
|
"destination": {
|
||||||
|
"name": "Home",
|
||||||
|
"query": "1 Bagshot Row, Hobbiton"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"action": {
|
||||||
|
"action": "add_stop",
|
||||||
|
"summary": "Add the petrol station",
|
||||||
|
"stop": {
|
||||||
|
"query": "Fuel station, Bywater"
|
||||||
|
},
|
||||||
|
"destination": {
|
||||||
|
"name": "Home",
|
||||||
|
"query": "1 Bagshot Row, Hobbiton"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A mode the schema does not have.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "navigate",
|
||||||
|
"summary": "Go",
|
||||||
|
"destination": {
|
||||||
|
"name": "Home",
|
||||||
|
"query": "1 Bagshot Row, Hobbiton"
|
||||||
|
},
|
||||||
|
"mode": "fly"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "mode is not one of drive, walk, bike, transit. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "search_nearby along the route.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "search_nearby",
|
||||||
|
"summary": "Petrol on the way",
|
||||||
|
"query": "petrol station",
|
||||||
|
"near": "route"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"action": {
|
||||||
|
"action": "search_nearby",
|
||||||
|
"summary": "Petrol on the way",
|
||||||
|
"query": "petrol station",
|
||||||
|
"near": "route"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "stop_navigation needs nothing.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "stop_navigation",
|
||||||
|
"summary": "Stop the route"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"action": {
|
||||||
|
"action": "stop_navigation",
|
||||||
|
"summary": "Stop the route"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "The checks run in the spec's order: time is reported before days.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "alarm",
|
||||||
|
"summary": "Alarm",
|
||||||
|
"time": "7",
|
||||||
|
"days": [
|
||||||
|
"monday"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "time is not a time as HH:MM (24-hour). Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A field another action reads is not checked here.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "media",
|
||||||
|
"summary": "Pause",
|
||||||
|
"command": "pause",
|
||||||
|
"url": "nope"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"action": {
|
||||||
|
"action": "media",
|
||||||
|
"summary": "Pause",
|
||||||
|
"command": "pause"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "An automation with its values.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "automation_intent",
|
||||||
|
"summary": "Run Arrive home",
|
||||||
|
"task": "Arrive home",
|
||||||
|
"values": {
|
||||||
|
"lights": "on"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"action": {
|
||||||
|
"action": "automation_intent",
|
||||||
|
"summary": "Run Arrive home",
|
||||||
|
"task": "Arrive home",
|
||||||
|
"values": {
|
||||||
|
"lights": "on"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Values must be text.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "automation_intent",
|
||||||
|
"summary": "Run",
|
||||||
|
"task": "x",
|
||||||
|
"values": {
|
||||||
|
"n": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "values is not named text values. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "An email draft with a list of recipients.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "email_draft",
|
||||||
|
"summary": "Email",
|
||||||
|
"to": [
|
||||||
|
"a@example.org"
|
||||||
|
],
|
||||||
|
"subject": "Hi"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"action": {
|
||||||
|
"action": "email_draft",
|
||||||
|
"summary": "Email",
|
||||||
|
"to": [
|
||||||
|
"a@example.org"
|
||||||
|
],
|
||||||
|
"subject": "Hi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A recipient list holding an empty name.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "email_draft",
|
||||||
|
"summary": "Email",
|
||||||
|
"to": [
|
||||||
|
"a@example.org",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"subject": "Hi"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "to is not a list of recipients. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "torch: on must be a boolean.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "torch",
|
||||||
|
"summary": "Torch",
|
||||||
|
"on": "yes"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "on is not true or false. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A panel the schema does not have.",
|
||||||
|
"kind": "check",
|
||||||
|
"args": {
|
||||||
|
"action": "settings_panel",
|
||||||
|
"summary": "Open",
|
||||||
|
"panel": "battery"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"refused": "panel is not one of wifi, bluetooth, internet, volume, nfc, location, do_not_disturb. Nothing was sent to the phone."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Done, with nothing said.",
|
||||||
|
"kind": "result",
|
||||||
|
"status": "done",
|
||||||
|
"message": "",
|
||||||
|
"expect": "Done on the phone."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Done, with what the phone said.",
|
||||||
|
"kind": "result",
|
||||||
|
"status": "done",
|
||||||
|
"message": "Alarm set for 07:00, weekdays.",
|
||||||
|
"expect": "Done on the phone.\n\nThe phone says: Alarm set for 07:00, weekdays."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Declined.",
|
||||||
|
"kind": "result",
|
||||||
|
"status": "declined",
|
||||||
|
"message": "",
|
||||||
|
"expect": "The person declined it on the phone: nothing was done. Do not try again unless they ask."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Declined, with the person's words.",
|
||||||
|
"kind": "result",
|
||||||
|
"status": "declined",
|
||||||
|
"message": "Not now.",
|
||||||
|
"expect": "The person declined it on the phone: nothing was done. Do not try again unless they ask.\n\nThe phone says: Not now."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Failed, with why.",
|
||||||
|
"kind": "result",
|
||||||
|
"status": "failed",
|
||||||
|
"message": "OsmAnd is not installed.",
|
||||||
|
"expect": "The phone could not do it: nothing was done.\n\nThe phone says: OsmAnd is not installed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A status the phone does not know is failed.",
|
||||||
|
"kind": "result",
|
||||||
|
"status": "maybe",
|
||||||
|
"message": "?",
|
||||||
|
"expect": "The phone could not do it: nothing was done.\n\nThe phone says: ?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A message is trimmed.",
|
||||||
|
"kind": "result",
|
||||||
|
"status": "done",
|
||||||
|
"message": " Torch on. \n",
|
||||||
|
"expect": "Done on the phone.\n\nThe phone says: Torch on."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "No answer in time.",
|
||||||
|
"kind": "result",
|
||||||
|
"status": null,
|
||||||
|
"message": "",
|
||||||
|
"expect": "The phone did not answer in time: nothing was done."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A message is cut at message_max_chars.",
|
||||||
|
"kind": "result",
|
||||||
|
"status": "failed",
|
||||||
|
"message": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"expect": "The phone could not do it: nothing was done.\n\nThe phone says: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,535 @@
|
|||||||
|
{
|
||||||
|
"about": "The prefix (context.md §10, spec 3.1.0): one session's requests, in order — which system prompt each sends, which tools it offers, and the note on a user message. `start` is the session as it begins: `date`, `mode`, and what the prompt is made from that may change (`plan` as rendered, `memory`, `personality`, `files` — instruction texts by path or heading — `mcp` — instructions by server; each \"\" or {} for none), `could` (the requirements the session could ever meet: `plan` for any session that may enter plan mode, `display`, `page`, `forge`) and `met` (those met now besides plan, which is met in plan mode; default: all it could). `tools`: the names a case looks at; a tool's `requires` is the spec's. Steps: `user` (a user message: the request that carries it), `tool` (a tool step's request: no user message), `set` (something changes — any of the start's fields, or `met`; `by: model` when the model's own tool did it, whose result told the model: `plan` from plan_write, `mode` from plan_submit's approval, `memory` and `files` it changed itself; otherwise the person or the clock did), `call` (a call of `tool` now), `compact` (the summary is asked for: `{{transcript}}` and `{{previous_summary}}` empty — an earlier summary is already the first message), `wrapup` (the request after a step limit or a budget: the same prompt and tools, the wrap-up message as its last user message, no note; a call in its reply is not run) and `model` (the session moves to another model). A request answers `{system, tools, note}`: `system` numbers the system prompts the session has made, from 1, the same number being the same bytes; `tools` the case's tools it offers; `note` the note after the person's words, or null. `call` answers the tool's `unavailable` text, or null where it runs; `compact` answers the request's prompt, its tools and `ask`, the user message that follows every message as it was; `wrapup` answers like a request; `set` and `model` answer null; a value that became empty reads `context.note.empty`. `plan_dir` fills modes/plan.md. LLeMbas: services/context.py; CLI: session/prefix.ts, session/engine.ts.",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"about": "A date that changes during a session is a note on the next user message; the system prompt and the tools stay the same bytes.",
|
||||||
|
"start": {
|
||||||
|
"date": "2026-11-04",
|
||||||
|
"mode": "manual",
|
||||||
|
"could": [
|
||||||
|
"plan"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "tool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "set",
|
||||||
|
"date": "2026-11-05"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "tool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\nToday is 2026-11-04.\n</session-note>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": null
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "plan_submit is offered in every mode and answers its unavailable text outside plan mode; a mode the person chose is a note with the mode's text, one the approval set is not.",
|
||||||
|
"start": {
|
||||||
|
"date": "2026-11-05",
|
||||||
|
"mode": "manual",
|
||||||
|
"could": [
|
||||||
|
"plan"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_write",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "call",
|
||||||
|
"tool": "plan_submit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "set",
|
||||||
|
"mode": "plan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "call",
|
||||||
|
"tool": "plan_submit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "set",
|
||||||
|
"mode": "edit",
|
||||||
|
"by": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "tool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_write",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
|
||||||
|
},
|
||||||
|
"Not in plan mode: plan_submit is how a plan made in plan mode is put to the person for approval. Carry on with the work; keep the plan current with plan_update.",
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_write",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\nThe permission mode is now plan:\nPermission mode: plan. You are planning, not implementing. Read, search and investigate as much as you need, but change nothing: no file but a plan under .agent/plans, no command that is not already allowed, nothing saved to the library.\n\nWrite the plan with plan_write, so that someone else could follow it: findings with file:line references, what will be true when it is done (objectives), and the work as phases of tasks in order, with the risks and how to verify it as the last phase. Ask with ask_user if a decision is the user's. When the plan is written, call plan_submit — that is how the user approves it, and after approval you carry on and implement it, keeping the plan current with plan_update.\n</session-note>"
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_write",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_write",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "The compaction request is the same prompt and tools, every message, then the compaction text with no transcript; after it the prompt is made again and the next user message says the date again.",
|
||||||
|
"start": {
|
||||||
|
"date": "2026-11-05",
|
||||||
|
"mode": "edit",
|
||||||
|
"could": [
|
||||||
|
"plan"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "tool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "compact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "compact"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"ask": "Summarise the conversation below so it can be carried forward after the earlier turns are dropped from your context. This is a working record, not a report for a reader.\n\nKeep, under these headings and in this order:\n\n## What we are doing\nThe goal, and where we have got to.\n\n## Decisions\nAnything settled, and why. A decision without its reason gets argued again.\n\n## Facts established\nNames, numbers, versions, file paths, commands, URLs and identifiers, copied exactly. Do not round them, paraphrase them or reconstruct one from memory — if it is not in the transcript, leave it out.\n\n## Files touched\nEach file read or changed that still matters, and what was done to it.\n\n## Open threads\nWhat is unfinished, and what was about to happen next.\n\nLeave out pleasantries, retracted ideas and anything already superseded. Do not answer the conversation: you are recording it. Write in the language of the conversation, and stay under 600 words."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": 2,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": 2,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"ask": "Summarise the conversation below so it can be carried forward after the earlier turns are dropped from your context. This is a working record, not a report for a reader.\n\nKeep, under these headings and in this order:\n\n## What we are doing\nThe goal, and where we have got to.\n\n## Decisions\nAnything settled, and why. A decision without its reason gets argued again.\n\n## Facts established\nNames, numbers, versions, file paths, commands, URLs and identifiers, copied exactly. Do not round them, paraphrase them or reconstruct one from memory — if it is not in the transcript, leave it out.\n\n## Files touched\nEach file read or changed that still matters, and what was done to it.\n\n## Open threads\nWhat is unfinished, and what was about to happen next.\n\nLeave out pleasantries, retracted ideas and anything already superseded. Do not answer the conversation: you are recording it. Write in the language of the conversation, and stay under 600 words."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "What the model changed itself is not said again; what the person changed is, once, in loop.json's order; an MCP server that connected later is said by its own instructions.",
|
||||||
|
"start": {
|
||||||
|
"date": "2026-11-05",
|
||||||
|
"mode": "manual",
|
||||||
|
"plan": "",
|
||||||
|
"memory": "Prefers tabs.",
|
||||||
|
"personality": "",
|
||||||
|
"files": {
|
||||||
|
"/p/AGENTS.md": "Build with make."
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"docs": "Search the docs first."
|
||||||
|
},
|
||||||
|
"could": [
|
||||||
|
"plan"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tools": [
|
||||||
|
"read"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "set",
|
||||||
|
"by": "model",
|
||||||
|
"plan": "# Fix\n## p1 P (pending)\n[t1] [ ] a",
|
||||||
|
"memory": "Prefers tabs.\n§\nUses fish.",
|
||||||
|
"files": {
|
||||||
|
"/p/AGENTS.md": "Build with make; test with make test."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "tool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "set",
|
||||||
|
"plan": "",
|
||||||
|
"memory": "Uses fish.",
|
||||||
|
"personality": "Be brief.",
|
||||||
|
"files": {
|
||||||
|
"/p/AGENTS.md": "Build with make; test with make test.",
|
||||||
|
"/p/src/AGENTS.md": "Strict types."
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"docs": "Search the docs first.",
|
||||||
|
"tickets": "Name the ticket in every commit."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "set",
|
||||||
|
"files": {
|
||||||
|
"/p/AGENTS.md": "Build with make; test with make test."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read"
|
||||||
|
],
|
||||||
|
"note": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read"
|
||||||
|
],
|
||||||
|
"note": null
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\nThe person changed the plan; it is now:\n(none)\n\nThe person changed what is remembered; it is now:\nUses fish.\n\n/p/src/AGENTS.md changed; it now reads:\nStrict types.\n\nThe person chose a different personality:\nBe brief.\n\nInstructions from an MCP server that connected since the session began:\n<server name=\"tickets\">\nName the ticket in every commit.\n</server>\n</session-note>"
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\n/p/src/AGENTS.md changed; it now reads:\n(none)\n</session-note>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A move to another model makes the prompt again; the date said in the conversation still holds.",
|
||||||
|
"start": {
|
||||||
|
"date": "2026-11-05",
|
||||||
|
"mode": "auto",
|
||||||
|
"could": [
|
||||||
|
"plan"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tools": [
|
||||||
|
"read"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"system": 2,
|
||||||
|
"tools": [
|
||||||
|
"read"
|
||||||
|
],
|
||||||
|
"note": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A display the session could have is offered from the start and answers unavailable while there is none; one it never could have leaves show out.",
|
||||||
|
"start": {
|
||||||
|
"date": "2026-11-05",
|
||||||
|
"mode": "manual",
|
||||||
|
"could": [
|
||||||
|
"plan",
|
||||||
|
"display"
|
||||||
|
],
|
||||||
|
"met": []
|
||||||
|
},
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"show"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "call",
|
||||||
|
"tool": "show"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "set",
|
||||||
|
"met": [
|
||||||
|
"display"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "call",
|
||||||
|
"tool": "show"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"show"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
|
||||||
|
},
|
||||||
|
"Nothing can be shown now: this session has no screen to show a file on. Give the user the path instead.",
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": {
|
||||||
|
"date": "2026-11-05",
|
||||||
|
"mode": "manual",
|
||||||
|
"could": [
|
||||||
|
"plan"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"show"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "The wrap-up keeps the session's tool list and prompt, carries no note, and the next user message carries what changed meanwhile.",
|
||||||
|
"start": {
|
||||||
|
"date": "2026-11-05",
|
||||||
|
"mode": "edit",
|
||||||
|
"could": [
|
||||||
|
"plan"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "tool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "set",
|
||||||
|
"date": "2026-11-06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "wrapup"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "user"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": null
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": 1,
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"plan_submit"
|
||||||
|
],
|
||||||
|
"note": "<session-note>\nToday is 2026-11-06.\n</session-note>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
{
|
||||||
|
"about": "What a request sends back of the model's reasoning (context.md §8, spec 3.2.0). `messages` is the history a request is built from, in order: `role` user, assistant or tool; `content`; an assistant message's stored `reasoning` (absent: it came with none); a user message's `turn` — false for a delivery at a step boundary (a job's note, a team message, words added to a running turn), which does not start a turn; absent or true for one that does (the person's, or a machine message starting a turn of its own). The answer is the messages as the OpenAI chat dialect sends them — `role`, `content`, and `reasoning_content` on an assistant message after the last user message that started a turn, when its reasoning is not empty (tool calls and ids are left out of the cases: they are sent as always). The Anthropic dialect sends the same messages' thinking blocks. LLeMbas: services/llm/openai_client.py, services/generation.py; CLI: provider/openai-chat.ts, session/engine.ts.",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"about": "One turn, three steps: every reply of the turn carries its reasoning back.",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Fix the failing test."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning": "The test name points at the parser."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0001 src/parse.ts lines 1-40 of 40 sha 1c9e04ab]\n…"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning": "Line 12 drops the last token."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0002 src/parse.ts sha 7d0a91c3]\n@@ -12 +12 @@"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Fix the failing test."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning_content": "The test name points at the parser."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0001 src/parse.ts lines 1-40 of 40 sha 1c9e04ab]\n…"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning_content": "Line 12 drops the last token."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0002 src/parse.ts sha 7d0a91c3]\n@@ -12 +12 @@"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A second turn: the first turn's replies go without their reasoning, the current one's with it.",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Where is the config loaded?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning": "Search for load."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0001] src/config/load.ts:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "In src/config/load.ts.",
|
||||||
|
"reasoning": "Found it at line 40."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "And who calls it?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning": "Grep for loadConfig."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0002] src/app.ts:120"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Where is the config loaded?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0001] src/config/load.ts:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "In src/config/load.ts."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "And who calls it?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning_content": "Grep for loadConfig."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0002] src/app.ts:120"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A job's note delivered at a step boundary does not start a turn: the turn's earlier replies keep their reasoning.",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Run the tests in the background and fix the lint."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning": "Start the tests, then lint."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0001] job1 started"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "[A background job you started has finished:]\njob1 (npm test) exited with 0 after 5s.",
|
||||||
|
"turn": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning": "Tests pass; now the lint."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0002] 0 problems"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Run the tests in the background and fix the lint."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning_content": "Start the tests, then lint."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0001] job1 started"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "[A background job you started has finished:]\njob1 (npm test) exited with 0 after 5s."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning_content": "Tests pass; now the lint."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0002] 0 problems"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A machine message that starts a turn of its own (a team report to an idle lead) does: the replies before it go without.",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Split the work."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Started alice.",
|
||||||
|
"reasoning": "Add one member."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "[Team report from alice to lead:]\nDone.",
|
||||||
|
"turn": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning": "Review alice's change."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0001 src/a.ts lines 1-10 of 10 sha 52b6e3a0]\n…"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Split the work."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Started alice."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "[Team report from alice to lead:]\nDone."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning_content": "Review alice's change."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0001 src/a.ts lines 1-10 of 10 sha 52b6e3a0]\n…"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A reply that came with no reasoning, or an empty one, carries no field.",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Hello"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"reasoning": "Greet."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0001] ok"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Hi."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Again"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Hi again.",
|
||||||
|
"reasoning": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Hello"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": "[#0001] ok"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Hi."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Again"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Hi again."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "The last message the person wrote starts the turn even when nothing has answered it yet: every earlier reply goes without.",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "One"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Done.",
|
||||||
|
"reasoning": "Easy."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Two"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expect": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "One"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Done."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Two"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
{
|
||||||
|
"about": "Spec 3.5.0 (acp.md *Scheduled work on a linked computer*): what a device makes of a scheduled turn. `accept`: the device's Accept scheduled work (remote.scheduled; false means remote work is off as well, the same refusal); `mode`: the schedule's (absent: none chosen); `max_mode`: remote.max_mode; `approval_wait_s`: the schedule's wait (absent: none). → `{mode, wait_s}` — the session's mode and how long an approval waits — or `{refused}` with the `data.reason` of the -32602 (`scheduled_off`, `plan`), whose message is loop.json scheduled.texts.off / .plan. Written from the rules; the device's other refusals (folder, trust) are the agent chat's.",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"about": "Off by default: refused, whatever else is asked.",
|
||||||
|
"accept": false,
|
||||||
|
"mode": "edit",
|
||||||
|
"max_mode": "edit",
|
||||||
|
"expect": {
|
||||||
|
"refused": "scheduled_off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "No mode chosen: Edit.",
|
||||||
|
"accept": true,
|
||||||
|
"max_mode": "edit",
|
||||||
|
"expect": {
|
||||||
|
"mode": "edit",
|
||||||
|
"wait_s": 1800
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "No mode chosen, the device allows Auto: still Edit — Auto only when the schedule says so.",
|
||||||
|
"accept": true,
|
||||||
|
"max_mode": "auto",
|
||||||
|
"expect": {
|
||||||
|
"mode": "edit",
|
||||||
|
"wait_s": 1800
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Auto chosen and allowed.",
|
||||||
|
"accept": true,
|
||||||
|
"mode": "auto",
|
||||||
|
"max_mode": "auto",
|
||||||
|
"expect": {
|
||||||
|
"mode": "auto",
|
||||||
|
"wait_s": 1800
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Auto chosen, the device allows Edit at most: lowered.",
|
||||||
|
"accept": true,
|
||||||
|
"mode": "auto",
|
||||||
|
"max_mode": "edit",
|
||||||
|
"expect": {
|
||||||
|
"mode": "edit",
|
||||||
|
"wait_s": 1800
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Edit chosen, the device allows Manual at most: lowered.",
|
||||||
|
"accept": true,
|
||||||
|
"mode": "edit",
|
||||||
|
"max_mode": "manual",
|
||||||
|
"expect": {
|
||||||
|
"mode": "manual",
|
||||||
|
"wait_s": 1800
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Manual chosen.",
|
||||||
|
"accept": true,
|
||||||
|
"mode": "manual",
|
||||||
|
"max_mode": "auto",
|
||||||
|
"expect": {
|
||||||
|
"mode": "manual",
|
||||||
|
"wait_s": 1800
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Plan is refused: nobody is there to approve a plan.",
|
||||||
|
"accept": true,
|
||||||
|
"mode": "plan",
|
||||||
|
"max_mode": "auto",
|
||||||
|
"expect": {
|
||||||
|
"refused": "plan"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "An unknown mode is read as the default, never as anything wider.",
|
||||||
|
"accept": true,
|
||||||
|
"mode": "unrestricted",
|
||||||
|
"max_mode": "auto",
|
||||||
|
"expect": {
|
||||||
|
"mode": "edit",
|
||||||
|
"wait_s": 1800
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "The device allows Plan at most: every scheduled turn is lowered to it.",
|
||||||
|
"accept": true,
|
||||||
|
"mode": "edit",
|
||||||
|
"max_mode": "plan",
|
||||||
|
"expect": {
|
||||||
|
"mode": "plan",
|
||||||
|
"wait_s": 1800
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "The wait, in seconds, as the schedule says.",
|
||||||
|
"accept": true,
|
||||||
|
"mode": "edit",
|
||||||
|
"max_mode": "edit",
|
||||||
|
"approval_wait_s": 600,
|
||||||
|
"expect": {
|
||||||
|
"mode": "edit",
|
||||||
|
"wait_s": 600
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Below the floor: one minute.",
|
||||||
|
"accept": true,
|
||||||
|
"mode": "edit",
|
||||||
|
"max_mode": "edit",
|
||||||
|
"approval_wait_s": 5,
|
||||||
|
"expect": {
|
||||||
|
"mode": "edit",
|
||||||
|
"wait_s": 60
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Above the ceiling: a day.",
|
||||||
|
"accept": true,
|
||||||
|
"mode": "edit",
|
||||||
|
"max_mode": "edit",
|
||||||
|
"approval_wait_s": 999999,
|
||||||
|
"expect": {
|
||||||
|
"mode": "edit",
|
||||||
|
"wait_s": 86400
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A fraction is cut to whole seconds.",
|
||||||
|
"accept": true,
|
||||||
|
"mode": "edit",
|
||||||
|
"max_mode": "edit",
|
||||||
|
"approval_wait_s": 90.7,
|
||||||
|
"expect": {
|
||||||
|
"mode": "edit",
|
||||||
|
"wait_s": 90
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"about": "Spec 3.5.0: whether a scheduled run's reply is `silent` — loop.json scheduled.sentinel on its first or last non-empty line, compared after trimming, any case, every trailing full stop forgiven — and the reply with every sentinel line taken out (`stripped`, the text a preview shows), trimmed. The server's rule for its own runs, and the device's for a scheduled turn.",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"about": "The sentinel alone.",
|
||||||
|
"text": "[SILENT]",
|
||||||
|
"expect": {
|
||||||
|
"silent": true,
|
||||||
|
"stripped": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Lower case, with a full stop.",
|
||||||
|
"text": "[silent].",
|
||||||
|
"expect": {
|
||||||
|
"silent": true,
|
||||||
|
"stripped": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "On the last line, after a sentence.",
|
||||||
|
"text": "Nothing new since yesterday.\n[SILENT]",
|
||||||
|
"expect": {
|
||||||
|
"silent": true,
|
||||||
|
"stripped": "Nothing new since yesterday."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "On the first line, before a sentence.",
|
||||||
|
"text": "[SILENT]\nChecked the feed: nothing.",
|
||||||
|
"expect": {
|
||||||
|
"silent": true,
|
||||||
|
"stripped": "Checked the feed: nothing."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "In the middle: not silent (only the first or last line counts), and the line still goes from the preview.",
|
||||||
|
"text": "Two new posts.\n[SILENT]\nBoth about waybread.",
|
||||||
|
"expect": {
|
||||||
|
"silent": false,
|
||||||
|
"stripped": "Two new posts.\nBoth about waybread."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Inside a sentence: not silent.",
|
||||||
|
"text": "I would say [SILENT] but there is news: the build broke.",
|
||||||
|
"expect": {
|
||||||
|
"silent": false,
|
||||||
|
"stripped": "I would say [SILENT] but there is news: the build broke."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Blank lines around it are not lines.",
|
||||||
|
"text": "\n\n [SILENT] \n\n",
|
||||||
|
"expect": {
|
||||||
|
"silent": true,
|
||||||
|
"stripped": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "A space before the full stop.",
|
||||||
|
"text": "All quiet.\n[SILENT] .",
|
||||||
|
"expect": {
|
||||||
|
"silent": true,
|
||||||
|
"stripped": "All quiet."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Without brackets: not the sentinel.",
|
||||||
|
"text": "SILENT",
|
||||||
|
"expect": {
|
||||||
|
"silent": false,
|
||||||
|
"stripped": "SILENT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Empty: not silent.",
|
||||||
|
"text": "",
|
||||||
|
"expect": {
|
||||||
|
"silent": false,
|
||||||
|
"stripped": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "An ordinary reply.",
|
||||||
|
"text": "The backup finished at 03:12; 4.2 GB.",
|
||||||
|
"expect": {
|
||||||
|
"silent": false,
|
||||||
|
"stripped": "The backup finished at 03:12; 4.2 GB."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"about": "Two full stops are forgiven too (every trailing one is).",
|
||||||
|
"text": "[SILENT]..",
|
||||||
|
"expect": {
|
||||||
|
"silent": true,
|
||||||
|
"stripped": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,288 @@
|
|||||||
|
# Context hygiene
|
||||||
|
|
||||||
|
Spec 3.2.0. What a session does to keep tool results from filling the model's context, and (§10, from
|
||||||
|
3.1.0) what it keeps still so the model's server can reuse what it has already read — the same in
|
||||||
|
a chat and in an agent session, in both projects. The texts and numbers are `loop.json` `context`;
|
||||||
|
the cases are `conformance/context.json`; what the model is told is `prompts/blocks/context.md`.
|
||||||
|
|
||||||
|
## The one rule
|
||||||
|
|
||||||
|
**Every measure acts when a result is inserted. Nothing already sent to the model is ever
|
||||||
|
rewritten.** Changing earlier context invalidates the prompt cache from that point on: a local
|
||||||
|
server processes everything after it again, and a cloud API charges full input price for it again —
|
||||||
|
on a long context that costs more than the measure saves. So no earlier result is ever shortened,
|
||||||
|
replaced by a stub or removed afterwards. The one deliberate break is compaction, which already
|
||||||
|
replaces the history with a summary. (From 3.2.0 a turn's reasoning is sent back during the turn and
|
||||||
|
not after it, §8: the common chat templates render the history the same either way.)
|
||||||
|
|
||||||
|
## 1. Every result has an id
|
||||||
|
|
||||||
|
Every tool result is given an id when it is inserted: `#` and the result's number in the session —
|
||||||
|
counting from 1, in lowercase hex, at least `ids.digits` (4) digits — so `#0001`, `#0002`, …,
|
||||||
|
`#000a`, …, `#10000`. Unique in the session and never reused — not after a compaction either. It is
|
||||||
|
the first thing in the result, in brackets: `[#0001]`, then a newline and the result.
|
||||||
|
|
||||||
|
A read's bracket says more: the path **as the call gave it**, the lines shown, the file's length in
|
||||||
|
lines and the first `sha_digits` (8) hex digits of the SHA-256 of the file's content —
|
||||||
|
|
||||||
|
```
|
||||||
|
[#0012 src/app.ts lines 1-200 of 340 sha 1c9e04ab]
|
||||||
|
1: import …
|
||||||
|
```
|
||||||
|
|
||||||
|
— and an empty file `[#0013 notes.txt empty sha e3b0c442]`. The full hash is kept by the session;
|
||||||
|
only it decides whether a file is unchanged.
|
||||||
|
|
||||||
|
**Paths in a bracket are the call's.** A read's bracket, an edit's (§5) and the compaction table
|
||||||
|
(§7) name the path as the call gave it — what the model wrote, so it recognises it. What identifies
|
||||||
|
the file — for a stub, for the table's "newest read of each path", for whether a file was read before
|
||||||
|
it is changed — is its absolute, resolved path (`filesystem.md`, *Paths*). Every other answer (an
|
||||||
|
error, a refusal, a search's results) names the absolute path.
|
||||||
|
|
||||||
|
**Every result is stored with the session, in full** — before any cap, and for a stub the text it
|
||||||
|
stands for — so `recall` can show any of them for as long as the session exists.
|
||||||
|
|
||||||
|
## 2. An unchanged re-read is a stub
|
||||||
|
|
||||||
|
**A stub names only a result that is in the request being built** — one whose text is among the
|
||||||
|
tool results this request sends to the model. That set, not anything the session stored, is the
|
||||||
|
dedupe table. Where a request does not carry earlier results — a chat on the server sends no earlier
|
||||||
|
reply's tool results with the next reply; after an undo, a rewind or a compaction they are gone — a
|
||||||
|
read is answered in full, because what a stub would point at is not in front of the model. It
|
||||||
|
holds for §2 and §6 alike.
|
||||||
|
|
||||||
|
A read is answered with a stub when the file's content hash equals that of an **earlier full read
|
||||||
|
of the same file still in the dedupe table**, and the lines asked for lie **wholly inside** that
|
||||||
|
read's lines:
|
||||||
|
|
||||||
|
```
|
||||||
|
[#0019 src/app.ts lines 50-80 of 340 sha 1c9e04ab] Unchanged since #0012, which already shows lines 1-200.
|
||||||
|
```
|
||||||
|
|
||||||
|
The bracket is the new read's; the sentence names the earlier result and its lines. A stub points
|
||||||
|
at a full result, never at another stub. A file that changed — by this session's own edit or from
|
||||||
|
outside — has another hash and is read in full; a range reaching past the earlier read is read in
|
||||||
|
full.
|
||||||
|
|
||||||
|
## 3. Large output is capped on the way in
|
||||||
|
|
||||||
|
A result over `cap.max_lines` (400) lines or `cap.max_chars` (40,000) characters keeps its head —
|
||||||
|
as many whole lines as fit `head_lines` (200) and `head_chars` (20,000) — and its tail — likewise,
|
||||||
|
`tail_lines` (100) and `tail_chars` (10,000) — with one line between:
|
||||||
|
|
||||||
|
```
|
||||||
|
[1234 lines elided, full output #0020]
|
||||||
|
```
|
||||||
|
|
||||||
|
A first line longer than `head_chars` is cut to it and ends ` […cut]`; the elided line follows it
|
||||||
|
whatever it counts. Reads and `recall` are not capped this way: each has its own window (2000 lines
|
||||||
|
from an offset).
|
||||||
|
|
||||||
|
## 4. `recall`
|
||||||
|
|
||||||
|
`recall(id, offset?, limit?)` shows a stored result again, in full, up to 2000 lines from `offset`:
|
||||||
|
|
||||||
|
```
|
||||||
|
[#0021 recall #0020 lines 1-2000 of 3400]
|
||||||
|
…
|
||||||
|
```
|
||||||
|
|
||||||
|
An id the session never gave: `[#0021] No stored result #ffff in this session.` The `#` may be left
|
||||||
|
out of the argument. A recall is not capped, is not deduplicated, and is itself stored.
|
||||||
|
|
||||||
|
## 5. Edits answer with the diff
|
||||||
|
|
||||||
|
`edit`, `multiedit`, `apply_patch` and `write` over an existing file answer with the unified diff
|
||||||
|
of the change (three lines of context, hunks only — the path is in the bracket) under a bracket
|
||||||
|
naming the path and the new content's hash:
|
||||||
|
|
||||||
|
```
|
||||||
|
[#0022 src/app.ts sha 7d0a91c3]
|
||||||
|
@@ -12,3 +12,3 @@
|
||||||
|
```
|
||||||
|
|
||||||
|
`write` of a new file answers `[#0023 docs/new.md sha 52b6e3a0] Created, 14 lines.` — the model
|
||||||
|
wrote the content and has it. A deleted file is `[#0024 old.md deleted]`. An `apply_patch` over
|
||||||
|
several files answers the id alone, then each file in its own bracket with its change, in the
|
||||||
|
patch's order:
|
||||||
|
|
||||||
|
```
|
||||||
|
[#0025]
|
||||||
|
[src/a.ts sha 7d0a91c3]
|
||||||
|
@@ -1,2 +1,2 @@
|
||||||
|
[docs/new.md sha 52b6e3a0] Created, 14 lines.
|
||||||
|
[old.md deleted]
|
||||||
|
```
|
||||||
|
|
||||||
|
A note the tool adds (a skill file written outside skill_manage) follows the diff. The diff is
|
||||||
|
capped like any output, the first line kept.
|
||||||
|
|
||||||
|
## 6. The same search or listing twice is a stub
|
||||||
|
|
||||||
|
For `dedupe.by_result` — `list`, `glob`, `grep`, `search` — a call with the same arguments (compared
|
||||||
|
as JSON with sorted keys) as an earlier one in the dedupe table, **whose result is the same text**,
|
||||||
|
is answered `[#0027] Same result as #0026.` The call is run again and compared, so a change from
|
||||||
|
anywhere shows up as a full result. Other tools are never deduplicated: a command's output twice is
|
||||||
|
two results.
|
||||||
|
|
||||||
|
## 7. Compaction
|
||||||
|
|
||||||
|
Compaction is the one deliberate break of the cache, and it already exists. **From 3.1.0 the
|
||||||
|
summary is asked for as the next turn of the same conversation**: the same system prompt, the same
|
||||||
|
tools, every message as it was, and then one more user message — `prompts/tasks/compact.md`, whose
|
||||||
|
`{{transcript}}` and `{{previous_summary}}` are empty in this form, since the model has the
|
||||||
|
conversation, an earlier summary included, itself. So the request
|
||||||
|
that makes the summary costs only that message; a separate request with a pasted transcript was a
|
||||||
|
full re-read of everything, and a transcript capped to fit it lost the session's middle. A tool
|
||||||
|
call in the summary's reply is not run; its text before the call is the summary, and a reply with
|
||||||
|
none fails the compaction as an empty one does. The summary request is never shown as a turn of the
|
||||||
|
session, and it is not stored as one. In 3.0.0 two things came with compaction, and still do:
|
||||||
|
|
||||||
|
- After the summary the harness adds — itself, not the model — a table of the **reads the summary
|
||||||
|
covers**: the full reads made since the previous compaction (or since the session began) in turns
|
||||||
|
that are still part of the history being summarised — whether or not their results are still in
|
||||||
|
the request. A turn taken back (an undo, a rewind, a reply regenerated or deleted) takes its reads
|
||||||
|
with it. Of those, the newest full read of each file (by its absolute path), newest last, at most
|
||||||
|
`compaction.rows` (50); a stub is not a read here. A session that resends every result (the CLI)
|
||||||
|
finds exactly these in its dedupe table; a chat, which does not, finds them in what it stored for
|
||||||
|
the turns being summarised:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Files read before this summary
|
||||||
|
|
||||||
|
Read a file again for its content; recall shows a result by its id.
|
||||||
|
|
||||||
|
| Path | Lines | sha | Result |
|
||||||
|
|---|---|---|---|
|
||||||
|
| src/app.ts | 1-200 of 340 | 1c9e04ab | #0012 |
|
||||||
|
```
|
||||||
|
|
||||||
|
With no reads there is no table.
|
||||||
|
- **The dedupe table is emptied**, so no stub ever points at content that is no longer in the
|
||||||
|
context. A read after compaction is a full read; a `recall` of any earlier id still works.
|
||||||
|
|
||||||
|
## 10. The prefix: what the model has already read
|
||||||
|
|
||||||
|
A model's server keeps what it has read of a request and reuses it for the next one only as far as
|
||||||
|
the two are the same from the first byte. The system prompt and the tool list come first, so **a
|
||||||
|
change to either is a full re-read** of everything after them — on a long session minutes of work
|
||||||
|
on a local server, and the full input price again on a cloud API. Measured (2026-11-05, a local
|
||||||
|
model): a date that changed at midnight sent a 16,912-token session from 99 % cached to 0 %,
|
||||||
|
139 seconds. So, from 3.1.0:
|
||||||
|
|
||||||
|
1. **The system prompt is made once per session and sent unchanged.** It is built when the session
|
||||||
|
starts, kept with the session, and every request sends those exact bytes. It is built again only
|
||||||
|
where everything is read again anyway: after a compaction (§7), or when the session moves to
|
||||||
|
another model. A session that is resumed sends what it kept.
|
||||||
|
2. **Nothing in it is a clock.** No date or time. The date goes in a note (rule 3): the first user
|
||||||
|
message of a session carries it, and so does the next one after the date has changed.
|
||||||
|
3. **What changes during a session goes at the end, as a note** on the next user message, never
|
||||||
|
into earlier text: the permission mode (`modes/<mode>.md` as the note's body when it changes,
|
||||||
|
and its first line is in the prompt only as it was at the start), the plan when the person changed
|
||||||
|
it (the model's own `plan_write` and `plan_update` already answer with it), a memory or
|
||||||
|
instruction file the person edited, a personality chosen, an MCP server's instructions that
|
||||||
|
arrived later, the date. The note is a block after the person's words:
|
||||||
|
|
||||||
|
```
|
||||||
|
<session-note>
|
||||||
|
The permission mode is now plan: …
|
||||||
|
</session-note>
|
||||||
|
```
|
||||||
|
|
||||||
|
(`loop.json` `context.note`.) Each change is one block of the note, the blocks separated by a
|
||||||
|
blank line, in the order `context.note` lists them; a value that became empty reads
|
||||||
|
`context.note.empty`; the date is the person's own (their zone, else the machine's). A note is
|
||||||
|
part of that message from then on, like anything sent. A request with no user message (a tool
|
||||||
|
step), and words the person adds to a turn already running, carry no note; it waits for the next
|
||||||
|
message that starts a turn. A change the model made itself (its own `plan_write`, `memory`,
|
||||||
|
`settings`, a file it wrote) needs no note: its tool result already said it.
|
||||||
|
4. **The tool list is the same for the whole session.** A tool whose `requires` can come and go
|
||||||
|
during a session — `page`, `display`, `forge`, and plan mode for `plan_submit` — is offered from
|
||||||
|
the start whenever the session could ever meet it, and a call made while it is not met answers
|
||||||
|
the tool's `unavailable` text. Only what is fixed for the session may leave a tool out: the
|
||||||
|
session kind (`sessions`), the model (`vision`), a subagent's narrowing, what the project's
|
||||||
|
configuration or the person switched off before the session began. The tools stay listed in
|
||||||
|
the wrap-up request too (`loop.json` `wrap_up`): its message says they are withdrawn, and a call
|
||||||
|
in that reply is not run. A tool withdrawn after its final refusal stays in the list and answers
|
||||||
|
that refusal. **The one exception is an MCP server**: a session waits up to `mcp_wait_ms` for
|
||||||
|
the servers it is configured with before its first request, so their tools are in the first
|
||||||
|
list; a server that connects later adds its tools when it connects — one re-read, once — and its
|
||||||
|
instructions come as a note.
|
||||||
|
5. **Nothing already sent is rewritten** — the one rule above, for results; old tool output is
|
||||||
|
never pruned in place.
|
||||||
|
6. **Every request's cache use is recorded** where the provider reports it (cached input tokens),
|
||||||
|
beside the request's other usage, so a slow prompt has a cause on record.
|
||||||
|
|
||||||
|
What a server does with a matching prefix is its own business; a harness that keeps these rules has
|
||||||
|
done what it can.
|
||||||
|
|
||||||
|
## 8. Reasoning goes back to the model during its turn
|
||||||
|
|
||||||
|
From 3.2.0 (it was left to the chat template before). A model that thinks before each tool call
|
||||||
|
works on from what it thought: sent its own reasoning back, it carries on; sent its calls without it,
|
||||||
|
it reasons its way to them again, step after step. Measured on a local model: a step re-read about a
|
||||||
|
fifth fewer tokens with its reasoning sent back.
|
||||||
|
|
||||||
|
- **The current turn** is every message after the last one that started a turn: a message the
|
||||||
|
person wrote, or a machine message that starts a turn of its own (a job's or a team's note to an
|
||||||
|
idle session, `loop.json` `jobs`). What is delivered at a step boundary while a turn runs — a
|
||||||
|
job's note, a team message, words the person adds — does not start a turn, and a tool result
|
||||||
|
never does.
|
||||||
|
- **Every assistant message of the current turn carries the reasoning it came with**, as the
|
||||||
|
model gave it, in full: in the OpenAI chat dialect as `reasoning_content` on the assistant message
|
||||||
|
(beside its `content` and `tool_calls`), and in the Anthropic dialect as its thinking blocks with
|
||||||
|
their signatures, unchanged and first (which that API already requires for tool use). Reasoning
|
||||||
|
read from inline tags (`<think>`; `quirks.json` `reasoning-fields`) goes back the same way, as
|
||||||
|
`reasoning_content`, and never as text in `content`. A message that came with no reasoning
|
||||||
|
carries none — no empty field.
|
||||||
|
- **Assistant messages of earlier turns carry none.** The common templates (Qwen3.x, Nemotron's
|
||||||
|
`truncate_history_thinking`, gpt-oss) drop it there themselves, so what the server reads of the
|
||||||
|
history is the same bytes either way and nothing is re-read; with a template that would keep it,
|
||||||
|
the turn boundary re-reads at most the previous turn. A compaction's summary starts the history
|
||||||
|
afresh: nothing before it carries reasoning.
|
||||||
|
- **What is stored is unchanged**: a session keeps each message's reasoning as before; this rule
|
||||||
|
is only about what a request carries (`conformance/reasoning.json`).
|
||||||
|
- A subagent, a team member and the main agent each have their own turns: a member's turn starts
|
||||||
|
with its brief or with a message that woke it.
|
||||||
|
|
||||||
|
## 11. No absolute paths in the prompt
|
||||||
|
|
||||||
|
From 3.2.0. An absolute path costs tokens on every request, says where a machine keeps its files,
|
||||||
|
and is copied by the model into calls where a relative one would do. So in an agent session (a
|
||||||
|
device's disk):
|
||||||
|
|
||||||
|
- **The system prompt, every note and the harness's own task messages (`tasks/init.md`) name no
|
||||||
|
absolute path of the machine.** The environment
|
||||||
|
block names the project by its root's last path component and says paths are relative to its
|
||||||
|
root (`loop.json` `context.env`): `Project: app (paths are relative to its root)`. Where the
|
||||||
|
session was started in a directory below the root, one more line names that directory relative
|
||||||
|
to the root: `Started in: packages/web (relative to the project root)`. There is no
|
||||||
|
`Working directory:` or `Project root:` line with an absolute path any more.
|
||||||
|
- **A file in the project is named relative to its root** (an instruction file, `docs/AGENTS.md`); a
|
||||||
|
file under the person's home directory outside the project as `~/…` (the global instructions,
|
||||||
|
`~/.config/lembas/AGENTS.md`). Only a file outside both is named by its absolute path.
|
||||||
|
- **Relative paths are resolved against the project root** — by every file tool (`read`, `write`,
|
||||||
|
`edit`, `multiedit`, `apply_patch`, `glob`, `grep`, `list`, `view_image`, `show`) and as the
|
||||||
|
directory `bash` starts in (its `cwd` argument, relative, likewise). Where the person started
|
||||||
|
below the root is a fact told to the model, not a second base; it still ends the instruction
|
||||||
|
chain (`filesystem.md`). **Absolute paths are still accepted** everywhere, as before, and answers
|
||||||
|
still name paths as `filesystem.md` says.
|
||||||
|
- A chat on the server is not a machine's disk: its paths are the server's tree's, short and the
|
||||||
|
same for every instance (`/home/<name>`, `blocks/workspace.md`). Its prompt names nothing longer.
|
||||||
|
|
||||||
|
`conformance/paths.json`: the environment lines, and how a path is resolved.
|
||||||
|
|
||||||
|
## 9. What the model is told
|
||||||
|
|
||||||
|
`prompts/blocks/context.md`, in every session that has `recall`.
|
||||||
|
|
||||||
|
## Not in 3.2.0
|
||||||
|
|
||||||
|
Eliding old content in batches behind a threshold, and measuring quality and tokens with and without
|
||||||
|
these measures, are left for a later spec. Every measure above ships with its unit and conformance
|
||||||
|
cases.
|
||||||
|
|
||||||
|
## Over the link
|
||||||
|
|
||||||
|
A device keeps its sessions' stored results itself; `recall` in an agent session reads the device's
|
||||||
|
store. The ids are in the result text, so a client showing the transcript shows them as they are.
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
# The file system a session works in
|
||||||
|
|
||||||
|
Spec 3.0.0. **Two worlds, one harness.** A **chat** (a conversation on the server) works in the
|
||||||
|
server's file system: a tree kept by the instance, which may hold repositories mounted from a forge.
|
||||||
|
An **agent** session (LLeMbas CLI, Desktop's agent, or an agent chat the web UI runs on a linked
|
||||||
|
device) works in its working directory on its own machine. Neither sees the other's files. What
|
||||||
|
the two share is this contract: how paths read, what kinds of node there are, how access is said,
|
||||||
|
what an error says, how a listing and a search look, and how instruction files are collected. The
|
||||||
|
tools are the same names and schemas on both sides (`tools/*.json`, `sessions`); where a tool exists
|
||||||
|
on one side only, it says so there.
|
||||||
|
|
||||||
|
The conformance areas are `fs_access` (the view and its errors) and `instructions` (the instruction
|
||||||
|
chain). An agent session's local disk is the special case of one node kind less (no mounts) and
|
||||||
|
one access level for the whole working directory, decided by the CLI's permission rules and modes
|
||||||
|
(`permission/`) rather than by the table below.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- **Absolute** paths begin with `/`, the root of the tree: the organisation's root on the server,
|
||||||
|
the file system's root on a device.
|
||||||
|
- **Relative** paths resolve against the session's **start directory** (its working directory) —
|
||||||
|
on a device, from spec 3.2.0, against the project root (*Node kinds*, below).
|
||||||
|
- `.` and `..` are resolved by their text, before anything is looked up; `..` above `/` stays at
|
||||||
|
`/`. Repeated `/` count as one, and a trailing `/` changes nothing. On the server there are no
|
||||||
|
links: a path is what it says. On a device, a link is followed, and where the CLI's rules look at
|
||||||
|
a path they look at where it really is (an `AGENTS.md` committed as a link to a file outside the
|
||||||
|
project is not read).
|
||||||
|
- Every answer names a path in its **absolute, resolved** form, whatever the call gave — an
|
||||||
|
error, a refusal, a search's results, a tool's own message. **The one exception is a result's
|
||||||
|
bracket** (`context.md`): a read's header, an edit's bracket and the compaction table name the
|
||||||
|
path as the call gave it, so the model recognises what it wrote. What identifies a file, there
|
||||||
|
too, is its absolute path. A listing names entries by their names under the directory listed
|
||||||
|
(*A listing*).
|
||||||
|
- Names are case-sensitive, `/`-separated, and never empty, `.` or `..`.
|
||||||
|
|
||||||
|
## Node kinds
|
||||||
|
|
||||||
|
| Kind | What |
|
||||||
|
|---|---|
|
||||||
|
| `dir` | a directory of the tree |
|
||||||
|
| `file` | a file: bytes, with a content hash (SHA-256) |
|
||||||
|
| `mount` | a directory that is a repository on a forge, at one branch. Below it, every node is a `dir` or a `file` of that repository. Server only |
|
||||||
|
|
||||||
|
A session has a **boundary** (what it may see at all: the root, a directory, one repository) and a
|
||||||
|
**start directory** inside it. On a device the boundary is the project root (the directory the
|
||||||
|
CLI's trust is given to) and the start directory is where the session started. From spec 3.2.0 a
|
||||||
|
device resolves relative paths against the **project root**, not the start directory (`context.md`
|
||||||
|
§11): the model is told the project and, when it started below the root, that directory relative to
|
||||||
|
the root — one base for every relative path, whatever the directory. The start directory still
|
||||||
|
ends the instruction chain (below).
|
||||||
|
|
||||||
|
## Access levels
|
||||||
|
|
||||||
|
Each path is at one of four levels for a session:
|
||||||
|
|
||||||
|
| Level | Name | Read | Change | Commit |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `none` | no access | no | no | no |
|
||||||
|
| `ro` | read-only | yes | no | no |
|
||||||
|
| `stage` | read/write, stage | yes | yes | no |
|
||||||
|
| `commit` | read/write, commit | yes | yes | yes |
|
||||||
|
|
||||||
|
- **Inside a mount**, `stage` means a change goes into the session's **working copy** (kept by the
|
||||||
|
instance) and stops there; `commit` means the session may also send it to the forge (`commit`,
|
||||||
|
`branch`, `pr`, `merge`, `tag`, `release`). Outside mounts there is nothing to commit, and the
|
||||||
|
two write levels mean the same: write.
|
||||||
|
- **A grant** names a path and a level, and covers everything below the path. **The most specific
|
||||||
|
grant wins**: `/` at `ro` and `/Notes` at `stage` make `/Notes/a.md` writable and `/b.md`
|
||||||
|
read-only; `/Notes/Private` at `none` takes that directory away again.
|
||||||
|
- **A floor** is a maximum level per path, set where the session cannot widen it (the data group's).
|
||||||
|
The most specific floor entry applies; the level a session has is its grant, lowered to the floor.
|
||||||
|
- A path with no grant on it or above it is at `none`. **Only what is at `ro` or above exists for
|
||||||
|
the model.** On a mount, the forge decides first: a level is never above what the user's own
|
||||||
|
forge account may do there.
|
||||||
|
- The levels a session has are told to the model (`prompts/blocks/workspace.md`, `{{view}}`): every
|
||||||
|
granted path with its level, so it knows the full paths it may use.
|
||||||
|
|
||||||
|
### A granted path under a path that is not
|
||||||
|
|
||||||
|
The tree `/AGENTS.md`, `/Dir1/`, `/Dir1/FILE1.txt`, `/Dir1/Dir3/`, `/Dir2/`, with `/AGENTS.md` and
|
||||||
|
`/Dir1/Dir3/` granted and nothing else:
|
||||||
|
|
||||||
|
- The model sees **`/AGENTS.md` and `/Dir1/Dir3/`**, each **by its full path**, and everything below
|
||||||
|
`/Dir1/Dir3/`.
|
||||||
|
- `/` and `/Dir1` are **passages**: at `none`, with something granted below. A passage is denied for
|
||||||
|
reading and writing alike: `list /Dir1`, `read /Dir1/FILE1.txt`, `write /Dir1/new.txt` and
|
||||||
|
`mkdir /Dir1/x` all answer `Access denied: … is not open to this session.`
|
||||||
|
- A listing never reveals a sibling: nothing lists `/Dir1`, and a listing of a granted directory
|
||||||
|
shows only its entries at `ro` or above.
|
||||||
|
- **A path the session cannot see is answered by what the model already knows**: by its nearest
|
||||||
|
ancestor that is either visible or a passage. Under a visible directory it is
|
||||||
|
`No such file or directory`; under a passage, `Access denied`. So `/Dir2/b.md` (hidden) and
|
||||||
|
`/Nope.md` (missing) get the same answer here — both under the passage `/` — and a hidden entry
|
||||||
|
under a granted directory reads exactly as a missing one.
|
||||||
|
- A search (`glob`, `grep`, `search`) rooted at a granted directory finds what is granted below it,
|
||||||
|
however it is reached; one rooted at a passage is denied like a listing.
|
||||||
|
- **An instruction file in a passage is not loaded** (below).
|
||||||
|
|
||||||
|
**One thing cannot be hidden**, and the spec accepts it: under a directory the session may write, a
|
||||||
|
name taken by an entry at `none` cannot be created. Making it answers `Access denied` for that path,
|
||||||
|
which says the name is taken and nothing else — not its kind, its size or what is in it.
|
||||||
|
|
||||||
|
## What each operation needs
|
||||||
|
|
||||||
|
| Operation | Tools | Needs |
|
||||||
|
|---|---|---|
|
||||||
|
| look | `list`, `read`, `glob`, `grep`, `search`, `show`, `python`'s `files`, `skill_view`, `skills_list` | `ro` |
|
||||||
|
| change | `write`, `edit`, `mkdir`, `mv` (the source and the target's directory), `rm`, `python`'s `save`, `skill_manage` | `stage` |
|
||||||
|
| commit | `commit`, `branch`, `pr`, `merge`, `tag`, `release` | `commit`, inside a mount |
|
||||||
|
|
||||||
|
A new path takes the level of the most specific grant above it. `mv` between a mount and the rest
|
||||||
|
of the tree, or between two mounts, is refused; `rm` of a mount itself is refused (a mount is
|
||||||
|
removed by whoever mounted it, not by a session). `rm` on the server moves to the **trash**,
|
||||||
|
restorable from the file explorer; inside a mount it stages the deletion.
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
A tool's error is its result (`loop.json` `parallel`). The texts, exactly, with `{path}` the
|
||||||
|
resolved absolute path:
|
||||||
|
|
||||||
|
| Case | Text |
|
||||||
|
|---|---|
|
||||||
|
| not visible (missing, or at `none`), and its nearest visible-or-passage ancestor is visible | `No such file or directory: {path}` |
|
||||||
|
| a passage; not visible with a passage as that ancestor; a name taken by an entry at `none` | `Access denied: {path} is not open to this session.` |
|
||||||
|
| a change at `ro` | `Read-only: {path} can be read in this session but not changed.` |
|
||||||
|
| a commit at `stage` | `Not committed: this session may change {path} but not commit it; the change stays staged.` |
|
||||||
|
| a commit outside every mount | `Nothing to commit: {path} is not in a repository.` |
|
||||||
|
| a directory where a file was meant | `Is a directory: {path}` |
|
||||||
|
| a file where a directory was meant | `Not a directory: {path}` |
|
||||||
|
| `mv`'s target, or a file `mkdir` was asked to make | `Already exists: {path}` |
|
||||||
|
| a tool whose `requires` is not met at the time of the call | the tool's `unavailable` text |
|
||||||
|
|
||||||
|
A missing file is `No such file or directory` for **every** tool that needs it to exist — `read`,
|
||||||
|
`edit`, `show`, `list`, `glob`, `grep`, `python`'s `files` — with no hint after it: in a chat a
|
||||||
|
hidden file must read exactly as a missing one, and a hint ("use write to create it") would say
|
||||||
|
which. The tool's description says what it needs.
|
||||||
|
|
||||||
|
`write` and `edit` refuse to change an existing file the session has not seen, or one that changed
|
||||||
|
since it last saw it (by reading it, or by its own change). The texts, exactly:
|
||||||
|
|
||||||
|
| Case | Text |
|
||||||
|
|---|---|
|
||||||
|
| the file exists and this session has not read it | `{path} exists and has not been read in this session. Read it first.` |
|
||||||
|
| it changed since this session last read or changed it | `{path} changed since you last read it. Read it again before changing it.` |
|
||||||
|
|
||||||
|
How a change is noticed is each side's: the content hash on the server, the modification time on a
|
||||||
|
device.
|
||||||
|
|
||||||
|
A device's own errors (a file too large, a binary file, a protected path) are the CLI's and stay as
|
||||||
|
they are; the ones above it says the same way.
|
||||||
|
|
||||||
|
## A listing
|
||||||
|
|
||||||
|
`list` and a directory given to `read`: one entry per line, directories (and mounts) with a trailing
|
||||||
|
`/`, then — in a chat — a mount marked ` [mount]` and an entry the session can read but not change,
|
||||||
|
in a directory it can, marked ` [read-only]`. Directories first, then by name in code-point order
|
||||||
|
(`B` before `a`). An empty directory is `(empty directory)` from `read`, `(empty)` from `list`.
|
||||||
|
|
||||||
|
`list` with a `depth` (default 2, at most 6) is a tree: the entries of the directory listed, and
|
||||||
|
under each directory the entries of that directory, each level indented by two more spaces, down to
|
||||||
|
`depth` levels in all (depth 1 is the directory's own entries). The order and the marks are a single
|
||||||
|
listing's, at every level; a directory's entries come right after it. At most 400 lines, then
|
||||||
|
`(more entries — list a directory further down)` on its own line. A device leaves out ignored and
|
||||||
|
build directories (`.git`, `node_modules`, …, and what `.gitignore` says) and shows only
|
||||||
|
directories that hold a file it would list.
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
guide/
|
||||||
|
index.md
|
||||||
|
repo/ [mount]
|
||||||
|
src/
|
||||||
|
README.md [read-only]
|
||||||
|
notes.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## A search
|
||||||
|
|
||||||
|
`search(query, path?, limit?)` ranks passages by meaning (embeddings) and by words (full-text) fused
|
||||||
|
by rank, then keeps only what the session's view allows — **after** scoring, so a hidden file
|
||||||
|
changes neither the results nor how many there are. A result is the passage's path and line range,
|
||||||
|
then the passage:
|
||||||
|
|
||||||
|
```
|
||||||
|
/Notes/setup.md:12-30
|
||||||
|
<the passage>
|
||||||
|
```
|
||||||
|
|
||||||
|
`grep` is the exact counterpart (text or a pattern), over the same view.
|
||||||
|
|
||||||
|
### Indexing, by tokens
|
||||||
|
|
||||||
|
What `search` reads is built when a file is written (a mount: its default branch, and again for the
|
||||||
|
files a new commit changes). The index is keyed by **content hash**, so a file in many places, or
|
||||||
|
unchanged across commits and branches, is embedded once; a rename or a move costs nothing.
|
||||||
|
|
||||||
|
Text is cut into pieces **by tokens, not by characters**: an embedder refuses input past its
|
||||||
|
context (512 tokens is common), and dense text — another language than English, code, hashes —
|
||||||
|
holds far more tokens per character than prose. A piece is at most **480 tokens** (`size`; room for
|
||||||
|
the embedder's own markers under 512) and repeats the last **64 tokens** (`overlap`) of the one
|
||||||
|
before it. The cut rules are the character splitter's (`conformance/chunk.json`) measured in tokens:
|
||||||
|
the window is the longest stretch of at most `size` tokens; the cut is just after a paragraph
|
||||||
|
break, else a line break, else a sentence's `. `, looked for in the window's last third (by
|
||||||
|
characters), else at the window's end; the next piece begins at the start of the token `overlap`
|
||||||
|
tokens before the cut. Pieces under 40 characters are dropped; a text that fits is one piece.
|
||||||
|
|
||||||
|
Tokens are counted with the embedder's own tokenizer where the server says how (llama.cpp's
|
||||||
|
`/tokenize`, for one). Where it does not, a count of `ceil(UTF-8 bytes / 2)` stands in — an
|
||||||
|
estimate, not a bound: it has not been measured against a real tokenizer. **A piece the embedder
|
||||||
|
still refuses for its length is split in two at the best cut and both are embedded; nothing is
|
||||||
|
dropped.**
|
||||||
|
`conformance/chunk_tokens.json` fixes the rules with a stand-in tokenizer (a token is a run of
|
||||||
|
non-whitespace), so both projects can be checked without an embedder.
|
||||||
|
|
||||||
|
## Instruction files, stacked
|
||||||
|
|
||||||
|
Instruction files are **collected from the boundary down to the start directory, outermost first**:
|
||||||
|
every directory on the way may contribute one, and all of them go to the model — the root's, then a
|
||||||
|
section's, then a repository's. (Before 3.0.0 the CLI took only the nearest one.)
|
||||||
|
|
||||||
|
- **The names**, in order of preference within one directory: `AGENTS.md`, then `CLAUDE.md`. A
|
||||||
|
directory contributes the first of them that is a file the session can read; never both.
|
||||||
|
- **Only what the session can read is loaded.** A file at `none` — in a passage, or taken away under
|
||||||
|
a granted directory — is not loaded, and the walk goes on below it: the next directory down may
|
||||||
|
still contribute. A file granted on its own (`/AGENTS.md` ticked while `/` is not) is loaded.
|
||||||
|
- A start directory outside the boundary is read as the boundary.
|
||||||
|
- On a device the boundary is the project root, a file counts only when it really is a file inside
|
||||||
|
the project (a link to elsewhere is not read), and the CLI's global `AGENTS.md` comes before the
|
||||||
|
chain, the `instructions` named in its configuration after it.
|
||||||
|
- **Below the start directory**, a file under a directory with its own instruction file hands the
|
||||||
|
model that file the first time something under it is read, once per session (the CLI's "nearby
|
||||||
|
instructions"), by the same reading rules.
|
||||||
|
- Each is given to the model under its path relative to the boundary.
|
||||||
|
|
||||||
|
`conformance/instructions.json` gives the cases, including the passage.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
A skill is a directory with a `SKILL.md` (frontmatter `name`, `description`), and `skills_list`,
|
||||||
|
`skill_view` and `skill_manage` are its meaning over files on both sides: the index in the prompt
|
||||||
|
(`prompts/blocks/skills.md`), `skill_view` to load one, `skill_manage` to make or change one, which
|
||||||
|
checks the frontmatter, the name and the size; `edit` works on the file too. Neither side syncs
|
||||||
|
skills on its own.
|
||||||
|
|
||||||
|
**Where they are found**, in order of precedence — a name found in an earlier place wins, and the
|
||||||
|
later one is not offered:
|
||||||
|
|
||||||
|
| Side | Places, the most specific first |
|
||||||
|
|---|---|
|
||||||
|
| chat | the `skills/` directory of every directory from the start directory up to the boundary, the nearest first, **the root's excluded**; then the person's own `/home/<user>/skills/`; then the organisation's `/skills/` (a skills repository mounted there, or a plain directory) |
|
||||||
|
| device | the project's `.agent/skills/` (a trusted project); then the user's own skills directory; then the directories the configuration adds, in its order |
|
||||||
|
|
||||||
|
So on both sides the session's own place comes first, the person's next, what is shared last. Only
|
||||||
|
what the session can read is a skill there; `skill_manage` makes a new one in the person's own
|
||||||
|
place.
|
||||||
|
|
||||||
|
**A skill's name**, which `skill_manage` checks when it makes or renames one: lowercase letters,
|
||||||
|
digits and `-`, starting with a letter or a digit, at most 64 characters —
|
||||||
|
`^[a-z0-9][a-z0-9-]{0,63}$`, the Agent Skills format's rule. A `SKILL.md` made elsewhere whose name
|
||||||
|
does not fit is still offered when its name is one line of at most 64 characters; a name is looked
|
||||||
|
up ignoring case, with `_` and spaces read as `-`.
|
||||||
|
|
||||||
|
## Memory
|
||||||
|
|
||||||
|
Memory is **records, not files**, and **the server keeps them for both sides**: a chat's memory is
|
||||||
|
its data group's partition, and a device logged in to an instance reads and writes the same records
|
||||||
|
through the instance (`acp.md`, *The HTTP API*), for a session the web UI opened in that chat's data
|
||||||
|
group, and for a session of its own in the group of the session's model. The `memory` tool is the
|
||||||
|
same tool on both sides (`tools/memory.json`); what each offers of its targets is its `narrow`.
|
||||||
+628
-2
@@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"wrap_up": {
|
"wrap_up": {
|
||||||
"about": "One more request with no tools and a user message saying which limit was reached: answer now — what is done, what is not, what would come next. The transcript says the prompt was stopped, so an answer given because the room ran out is never mistaken for one the model chose to give.",
|
"about": "One more request, with the session's tool list unchanged (context.md §10, rule 4) and a user message saying which limit was reached: answer now — what is done, what is not, what would come next. The transcript says the prompt was stopped, so an answer given because the room ran out is never mistaken for one the model chose to give. A tool call in that reply is not run.",
|
||||||
"message": "The {{limit}} for this prompt has been reached and the tools have been withdrawn. Answer now from what you have: what is done, what is not, and what would come next."
|
"message": "The {{limit}} for this prompt has been reached and the tools have been withdrawn. Answer now from what you have: what is done, what is not, and what would come next."
|
||||||
},
|
},
|
||||||
"nudges": {
|
"nudges": {
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"about": "Calls in one step run together, at most 4; a call that waits on the user (ask_user, plan_submit) runs alone; two calls on one file run in order. A tool's error is its result, never the end of the prompt.",
|
"about": "Calls in one step run together, at most 4; a call that waits on the user (ask_user, plan_submit) runs alone; two calls on one file run in order. A tool's error is its result, never the end of the prompt.",
|
||||||
"max": 4
|
"max": 4
|
||||||
},
|
},
|
||||||
"capacity": "A subagent (task) is not started on a model whose server cannot hold it beside the prompt's own model — or beside the model of a subagent already running: another model of a connection that holds one model at a time, or the same model when it serves one request at a time (models/schema.json capacity).",
|
"capacity": "A subagent (task) is not started on a model whose server cannot hold it beside the prompt's own model — or beside the model of a subagent already running: another model of a connection that holds one model at a time, or the same model when it already serves as many of this session's requests as it can at once (models/schema.json capacity: `parallel`, `single_session` being parallel 1). Spec 3.3.0: which model a subagent may run on at all, and the refusals for the session's own one-model server and a one-request model, are subagents.md and `subagents` below, checked first. A team's members are not refused for a full model: they wait for a slot (team.md, Capacity).",
|
||||||
"jobs": {
|
"jobs": {
|
||||||
"about": "Background work owned by a session: a command (bash `background: true`, or one still running when its timeout passed) or a subagent (task `background: true`). One id sequence per session (job1, job2, …), one list (job_list), one way to read (job_output), to stop (job_stop) and, for a subagent, to message (job_send); a subagent's own tools see only the jobs it started. A job that ends wakes the session that owns it: while a prompt runs, as a user message at the next step boundary; otherwise as a new turn of its own, marked `machine` so a UI shows it as the harness's, not the person's. A session that is not open keeps the note until it is resumed. Every such message begins with its mark, said rather than hidden: a model refuses an unmarked one as an injection (conformance/jobs.json gives the texts).",
|
"about": "Background work owned by a session: a command (bash `background: true`, or one still running when its timeout passed) or a subagent (task `background: true`). One id sequence per session (job1, job2, …), one list (job_list), one way to read (job_output), to stop (job_stop) and, for a subagent, to message (job_send); a subagent's own tools see only the jobs it started. A job that ends wakes the session that owns it: while a prompt runs, as a user message at the next step boundary; otherwise as a new turn of its own, marked `machine` so a UI shows it as the harness's, not the person's. A session that is not open keeps the note until it is resumed. Every such message begins with its mark, said rather than hidden: a model refuses an unmarked one as an injection (conformance/jobs.json gives the texts).",
|
||||||
"marks": {
|
"marks": {
|
||||||
@@ -60,5 +60,631 @@
|
|||||||
"default": 50
|
"default": 50
|
||||||
},
|
},
|
||||||
"lifecycle": "A subagent's own background commands stop when it ends, and its report says so. A session's jobs stop when the session is deleted. A front end asked to exit while jobs run asks the person first; one with nobody to ask waits for the jobs the prompt started, then stops the rest."
|
"lifecycle": "A subagent's own background commands stop when it ends, and its report says so. A session's jobs stop when the session is deleted. A front end asked to exit while jobs run asks the person first; one with nobody to ask waits for the jobs the prompt started, then stops the rest."
|
||||||
|
},
|
||||||
|
"subagents": {
|
||||||
|
"about": "Spec 3.3.0 (subagents.md): which model a subagent — a task subagent or a team member — runs on, and when subagents are offered. The session's own model unless the person named another in this session; no setting chooses one. The texts are said word for word, checked in this order: not_named, same_group, one_request. conformance/subagents.json.",
|
||||||
|
"texts": {
|
||||||
|
"not_named": "Use another model for subagents only when the person names it.",
|
||||||
|
"same_group": "That model is on the same one-model server as this chat's model; name a model on another server.",
|
||||||
|
"one_request": "This model serves one request at a time: name another model, on another server, for subagents."
|
||||||
|
},
|
||||||
|
"named": {
|
||||||
|
"about": "A model is named when its served id, its id at its connection or its display name occurs as a whole word, without case, in a message the person typed in this session (never model text, tool output, files, notes, marked messages or reports; a compacted message still counts). Before the name: not a letter, digit, _, -, /, . or :. After it: not a letter, digit, _, - or /, nor a . or : followed by a letter or digit. White space in a display name matches any run of white space.",
|
||||||
|
"before": "[\\p{L}\\p{N}_\\-/.:]",
|
||||||
|
"after": "[\\p{L}\\p{N}_\\-/]|[.:][\\p{L}\\p{N}]"
|
||||||
|
},
|
||||||
|
"setting": {
|
||||||
|
"about": "How the agents {min, max} setting (team.md §2) is shown to a person: one range from 1 to the cap with two handles, `min` and `max` naming them; `label` beside it, `hint` (or `short` where one line has no room) saying what the numbers count — the session's own agent included, so 1 is that agent alone and Max 4 is it and up to 3 subagents; and `own` beside it when the session's model cannot serve subagents itself (subagents.md §4).",
|
||||||
|
"label": "Subagents",
|
||||||
|
"min": "Min",
|
||||||
|
"max": "Max",
|
||||||
|
"hint": "The count includes this agent: 1 is this agent alone, with no subagents; Max 4 is this agent and up to 3 subagents.",
|
||||||
|
"short": "1 is this agent alone",
|
||||||
|
"own": "This model serves one request at a time: subagents need another model, on another server, named in the chat."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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; 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,
|
||||||
|
"max": 4,
|
||||||
|
"cap": 32
|
||||||
|
},
|
||||||
|
"names": {
|
||||||
|
"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",
|
||||||
|
"parent"
|
||||||
|
],
|
||||||
|
"parent": "parent",
|
||||||
|
"id_prefix": "s"
|
||||||
|
},
|
||||||
|
"marks": {
|
||||||
|
"message": "[Team message 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.",
|
||||||
|
"one_agent": "The person allows one agent in this chat: there is no team. Do the work yourself.",
|
||||||
|
"one_request": "This model serves one request at a time now, so there is no team. Do the work yourself."
|
||||||
|
},
|
||||||
|
"work": {
|
||||||
|
"about": "What a manager may not do itself (team.md §6): every call whose risk class is write or execute — an MCP tool's is read when it says readOnlyHint, else execute — but `job_stop`, and a `bash` call that the default rules alone allow in manual mode (reading commands). The tool list is unchanged; these answer texts.manager.",
|
||||||
|
"write": [
|
||||||
|
"apply_patch",
|
||||||
|
"edit",
|
||||||
|
"mkdir",
|
||||||
|
"multiedit",
|
||||||
|
"mv",
|
||||||
|
"rm",
|
||||||
|
"skill_manage",
|
||||||
|
"write"
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
"bash",
|
||||||
|
"branch",
|
||||||
|
"commit",
|
||||||
|
"merge",
|
||||||
|
"pr",
|
||||||
|
"python",
|
||||||
|
"release",
|
||||||
|
"tag"
|
||||||
|
],
|
||||||
|
"kept": [
|
||||||
|
"job_stop"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"read_max": 50,
|
||||||
|
"texts": {
|
||||||
|
"added": "Added {{name}} ({{role}}): it is working. Its report comes to you when it finishes. The team is {{size}} of {{max}} agents.",
|
||||||
|
"added_waiting": "Added {{name}} ({{role}}): it waits for a free slot — the model serves {{parallel}} requests at once and all are busy — and starts when one frees. Its report comes to you when it finishes. The team is {{size}} of {{max}} agents.",
|
||||||
|
"added_under": "Added {{name}} ({{role}}) under {{parent}}: it is working. Its report goes to {{parent}} when it finishes. The team is {{size}} of {{max}} agents.",
|
||||||
|
"added_waiting_under": "Added {{name}} ({{role}}) under {{parent}}: it waits for a free slot — the model serves {{parallel}} requests at once and all are busy — and starts when one frees. Its report goes to {{parent}} when it finishes. The team is {{size}} of {{max}} agents.",
|
||||||
|
"sent": "Sent to {{to}}.",
|
||||||
|
"stopped": "Stopped {{names}}. The team is {{size}} of {{max}} agents.",
|
||||||
|
"manager": "You lead a team: give this to a member (team_add or team_send).",
|
||||||
|
"full": "The team is full: it has as many agents as the person allows ({{max}}). Give the work to a member already on it (team_send), or stop one (team_stop) first.",
|
||||||
|
"bad_name": "{{name}} cannot be a name: use lowercase letters, digits and hyphens, beginning with a letter, at most 32 characters.",
|
||||||
|
"all_name": "all addresses the whole team; it cannot be a name.",
|
||||||
|
"taken": "There is already an agent called {{name}} in this team. Choose another name.",
|
||||||
|
"no_goal": "Say the team's goal in `goal`, in one line, with your first member: every member is told it.",
|
||||||
|
"unknown": "No agent called {{name}} in this team. The team: {{names}}.",
|
||||||
|
"self": "That is you: send it to someone else.",
|
||||||
|
"nobody": "Nobody else is on the team yet: add a member with team_add.",
|
||||||
|
"not_yours": "{{name}} is not under you: you can stop only the members you lead, directly or through your members.",
|
||||||
|
"nobody_to_stop": "No member is under you: there is nobody to stop.",
|
||||||
|
"no_report": "(no report)",
|
||||||
|
"ended": "[ended: {{reason}}]",
|
||||||
|
"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. 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}} ({{id}}) — {{role}} · {{state}} · {{model}} · {{mode}} · {{context}} · {{last}}",
|
||||||
|
"lead_role": "the main agent",
|
||||||
|
"states": {
|
||||||
|
"working": "working",
|
||||||
|
"waiting": "waiting for a slot",
|
||||||
|
"finished": "finished",
|
||||||
|
"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}}.",
|
||||||
|
"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": {
|
||||||
|
"about": "Context hygiene (spec 3.0.0, the prefix from 3.1.0; context.md): every measure acts when a result is inserted, and nothing already sent to the model is ever rewritten — changing earlier context throws away the prompt cache from that point. conformance/context.json gives the texts.",
|
||||||
|
"ids": {
|
||||||
|
"about": "Every tool result gets an id: `#` and its number in the session, counting from 1, in lowercase hex, at least 4 digits (`#0001`, `#000a`, `#10000`) — unique in the session and never reused (not after a compaction either), so `recall` finds any of them. It is the first thing in the result, in brackets.",
|
||||||
|
"digits": 4
|
||||||
|
},
|
||||||
|
"read_header": "[{{id}} {{path}} lines {{from}}-{{to}} of {{of}} sha {{sha}}]",
|
||||||
|
"read_header_empty": "[{{id}} {{path}} empty sha {{sha}}]",
|
||||||
|
"edit_header": "[{{id}} {{path}} sha {{sha}}]",
|
||||||
|
"header": "[{{id}}]",
|
||||||
|
"unchanged": "Unchanged since {{earlier}}, which already shows lines {{from}}-{{to}}.",
|
||||||
|
"same_result": "Same result as {{earlier}}.",
|
||||||
|
"elided": "[{{n}} lines elided, full output {{id}}]",
|
||||||
|
"created": "Created, {{n}} lines.",
|
||||||
|
"no_result": "No stored result {{target}} in this session.",
|
||||||
|
"recall_header": "[{{id}} recall {{target}} lines {{from}}-{{to}} of {{of}}]",
|
||||||
|
"sha_digits": 8,
|
||||||
|
"cap": {
|
||||||
|
"about": "A result over max_lines lines or max_chars characters keeps its head (as many whole lines as fit both head limits) and its tail (likewise, from what is left), with the elided line between. When even the first line does not fit head_chars, it is cut to it and ends ` […cut]`, and the elided line follows however many whole lines it counts (0 included): it is there whenever anything was left out. Not applied to read (its own window: 2000 lines from an offset) or recall (likewise). The full output is stored with the session.",
|
||||||
|
"max_lines": 400,
|
||||||
|
"max_chars": 40000,
|
||||||
|
"head_lines": 200,
|
||||||
|
"head_chars": 20000,
|
||||||
|
"tail_lines": 100,
|
||||||
|
"tail_chars": 10000
|
||||||
|
},
|
||||||
|
"dedupe": {
|
||||||
|
"about": "by_hash: a read whose file (by its absolute path) has the same content hash as an earlier full read in the table, and whose lines lie wholly inside that read's, is answered with `unchanged`. by_result: a call with the same arguments as an earlier one in the table, whose result is the same text, is answered with `same_result`. Stubs point at the full result, never at another stub. The table is the full results in the request being built — never one the request does not carry (a chat's earlier reply, a turn undone) — and compaction empties it.",
|
||||||
|
"by_hash": [
|
||||||
|
"read"
|
||||||
|
],
|
||||||
|
"by_result": [
|
||||||
|
"list",
|
||||||
|
"glob",
|
||||||
|
"grep",
|
||||||
|
"search"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"compaction": {
|
||||||
|
"about": "The one deliberate break of the cache. After the summary the harness adds the table of the reads the summary covers — the full reads made since the previous compaction, in turns still in the history being summarised, whether or not the request still carries them; of those the newest full read of each file, newest last, at most `rows` — and then empties the dedupe table, so no stub ever points at content that is gone from the context. No reads: no table.",
|
||||||
|
"heading": "## Files read before this summary",
|
||||||
|
"line": "Read a file again for its content; recall shows a result by its id.",
|
||||||
|
"rows": 50
|
||||||
|
},
|
||||||
|
"reasoning": "Spec 3.2.0 (context.md §8): every assistant message of the current turn — the messages after the last one that started a turn (the person's, or a machine message that starts a turn of its own; a delivery at a step boundary does not) — carries the reasoning it came with (OpenAI chat: `reasoning_content`; Anthropic: its thinking blocks, unchanged); assistant messages of earlier turns carry none. What is stored keeps it. conformance/reasoning.json.",
|
||||||
|
"env": {
|
||||||
|
"about": "Spec 3.2.0 (context.md §11): the environment block's lines about where the session works, in an agent session. `project` always, `name` being the project root's last path component; `started` only when the session started in a directory below the root, `path` being that directory relative to the root. No absolute path. conformance/paths.json.",
|
||||||
|
"project": "Project: {{name}} (paths are relative to its root)",
|
||||||
|
"started": "Started in: {{path}} (relative to the project root)"
|
||||||
|
},
|
||||||
|
"note": {
|
||||||
|
"about": "A note on the next user message (context.md §10, rule 3): what changed since the system prompt was made, after the person's words. Lines in this order, each only when it applies. `plan_ended` is the mode block for a session that leaves plan mode for a mode with no text of its own (a chat). `plan_own` is the plan block for a session that does not resend earlier tool results (a chat): the model's own plan, as its tools left it. `personal` is the {{path}} of the instructions block for the person's own instructions. Spec 3.2.0, where a team is offered (team.md §10): `team` — the size setting, on the first user message and after it changes, followed in the same block by `team_lead` when min ≥ 2 ({{members}} = min − 1), or `team_one` in its place when max is 1; `team_stopped` — members the person stopped, a block of its own. Spec 3.3.0 (subagents.md §4): `team_own` after `team` (and after `team_lead`) in the same block when the session's model cannot serve subagents itself — it serves one request at a time — and another server's model could.",
|
||||||
|
"open": "<session-note>",
|
||||||
|
"close": "</session-note>",
|
||||||
|
"date": "Today is {{date}}.",
|
||||||
|
"mode": "The permission mode is now {{mode}}:\n{{mode_text}}",
|
||||||
|
"plan_ended": "Plan mode has ended: make the changes the work needs; nothing is refused for planning any more.",
|
||||||
|
"plan": "The person changed the plan; it is now:\n{{plan}}",
|
||||||
|
"plan_own": "Your plan, as your plan tools left it:\n{{plan}}",
|
||||||
|
"memory": "The person changed what is remembered; it is now:\n{{memory}}",
|
||||||
|
"instructions": "{{path}} changed; it now reads:\n{{text}}",
|
||||||
|
"personal": "the person's own instructions",
|
||||||
|
"personality": "The person chose a different personality:\n{{personality}}",
|
||||||
|
"mcp": "Instructions from an MCP server that connected since the session began:\n{{instructions}}",
|
||||||
|
"team": "The team may have {{min}} to {{max}} agents, you included.",
|
||||||
|
"team_lead": "Work through 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.<phase> (a variant: progress.<phase>.<variant>); `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
|
||||||
|
},
|
||||||
|
"scheduled": {
|
||||||
|
"about": "Spec 3.5.0 (acp.md *Scheduled work on a linked computer*): a schedule's run as an agent turn on a linked computer, and the watch command of monitor mode. `setting` is how the device's switch is shown; `modes` the modes a schedule may choose (`default_mode` when it chose none), lowered to remote.max_mode; `approval_wait_seconds` how long an approval in a scheduled turn waits (the schedule's wait, clamped); `sentinel` the word a reply says when there is nothing to tell; `monitor` the watch command's limits. conformance/scheduled.json, silent.json, monitor.json.",
|
||||||
|
"setting": {
|
||||||
|
"key": "remote.scheduled",
|
||||||
|
"label": "Accept scheduled work",
|
||||||
|
"hint": "Let your schedules run here as agent turns, in the folders remote work may use. Off by default."
|
||||||
|
},
|
||||||
|
"modes": [
|
||||||
|
"manual",
|
||||||
|
"edit",
|
||||||
|
"auto"
|
||||||
|
],
|
||||||
|
"default_mode": "edit",
|
||||||
|
"approval_wait_seconds": {
|
||||||
|
"default": 1800,
|
||||||
|
"min": 60,
|
||||||
|
"max": 86400
|
||||||
|
},
|
||||||
|
"sentinel": "[SILENT]",
|
||||||
|
"statuses": [
|
||||||
|
"ok",
|
||||||
|
"silent",
|
||||||
|
"failed",
|
||||||
|
"cancelled"
|
||||||
|
],
|
||||||
|
"monitor": {
|
||||||
|
"timeout_seconds": {
|
||||||
|
"default": 60,
|
||||||
|
"min": 1,
|
||||||
|
"max": 600
|
||||||
|
},
|
||||||
|
"excerpt_chars": 2000,
|
||||||
|
"output_max_bytes": 1048576
|
||||||
|
},
|
||||||
|
"texts": {
|
||||||
|
"off": "This computer does not accept scheduled work. Turn on \"Accept scheduled work\" on it (lembas service scheduled on).",
|
||||||
|
"plan": "A scheduled run cannot use plan mode: nobody is there to approve a plan. Choose Manual, Edit or Auto.",
|
||||||
|
"denied": "Nobody answered this approval within {{minutes}} minutes, so it counts as denied.",
|
||||||
|
"monitor_asks": "The watch command would need approval in {{mode}} mode ({{reason}}), and a watch has nobody to ask. Allow the command in the project's permission rules, or choose a wider mode for the schedule.",
|
||||||
|
"monitor_denied": "The watch command is refused here: {{reason}}.",
|
||||||
|
"no_reply": "The run ended without a reply."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"about": "Spec 3.6.0 (phone.md): `phone_action`, carried out by the LLeMbas app on the person's phone and confirmed there. `capability` is what the app declares with each message it sends; `fields` what each action reads (anything else in a call is dropped before the phone sees it); `required` the fields each action needs (a list inside the list: any one of them); `check` the order the fields' values are checked in; `wait_seconds` how long a call waits for the phone's answer; `texts` what the model is told, `what` what a bad field should have been. conformance/phone.json.",
|
||||||
|
"capability": "phone_actions",
|
||||||
|
"actions": [
|
||||||
|
"alarm",
|
||||||
|
"timer",
|
||||||
|
"calendar_event",
|
||||||
|
"sms_draft",
|
||||||
|
"email_draft",
|
||||||
|
"dial",
|
||||||
|
"navigate",
|
||||||
|
"add_stop",
|
||||||
|
"search_nearby",
|
||||||
|
"stop_navigation",
|
||||||
|
"open_app",
|
||||||
|
"open_url",
|
||||||
|
"media",
|
||||||
|
"torch",
|
||||||
|
"settings_panel",
|
||||||
|
"automation_intent"
|
||||||
|
],
|
||||||
|
"fields": {
|
||||||
|
"alarm": [
|
||||||
|
"time",
|
||||||
|
"days",
|
||||||
|
"label"
|
||||||
|
],
|
||||||
|
"timer": [
|
||||||
|
"seconds",
|
||||||
|
"label"
|
||||||
|
],
|
||||||
|
"calendar_event": [
|
||||||
|
"title",
|
||||||
|
"start",
|
||||||
|
"end",
|
||||||
|
"all_day",
|
||||||
|
"location",
|
||||||
|
"notes"
|
||||||
|
],
|
||||||
|
"sms_draft": [
|
||||||
|
"to",
|
||||||
|
"body"
|
||||||
|
],
|
||||||
|
"email_draft": [
|
||||||
|
"to",
|
||||||
|
"cc",
|
||||||
|
"subject",
|
||||||
|
"body"
|
||||||
|
],
|
||||||
|
"dial": [
|
||||||
|
"number"
|
||||||
|
],
|
||||||
|
"navigate": [
|
||||||
|
"destination",
|
||||||
|
"stops",
|
||||||
|
"mode",
|
||||||
|
"replace_route"
|
||||||
|
],
|
||||||
|
"add_stop": [
|
||||||
|
"stop",
|
||||||
|
"destination"
|
||||||
|
],
|
||||||
|
"search_nearby": [
|
||||||
|
"query",
|
||||||
|
"near"
|
||||||
|
],
|
||||||
|
"stop_navigation": [],
|
||||||
|
"open_app": [
|
||||||
|
"app"
|
||||||
|
],
|
||||||
|
"open_url": [
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"media": [
|
||||||
|
"command"
|
||||||
|
],
|
||||||
|
"torch": [
|
||||||
|
"on"
|
||||||
|
],
|
||||||
|
"settings_panel": [
|
||||||
|
"panel"
|
||||||
|
],
|
||||||
|
"automation_intent": [
|
||||||
|
"task",
|
||||||
|
"app",
|
||||||
|
"values"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"alarm": [
|
||||||
|
"time"
|
||||||
|
],
|
||||||
|
"timer": [
|
||||||
|
"seconds"
|
||||||
|
],
|
||||||
|
"calendar_event": [
|
||||||
|
"title",
|
||||||
|
"start"
|
||||||
|
],
|
||||||
|
"sms_draft": [
|
||||||
|
[
|
||||||
|
"to",
|
||||||
|
"body"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"email_draft": [
|
||||||
|
[
|
||||||
|
"to",
|
||||||
|
"subject",
|
||||||
|
"body"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"dial": [
|
||||||
|
"number"
|
||||||
|
],
|
||||||
|
"navigate": [
|
||||||
|
"destination"
|
||||||
|
],
|
||||||
|
"add_stop": [
|
||||||
|
"stop",
|
||||||
|
"destination"
|
||||||
|
],
|
||||||
|
"search_nearby": [
|
||||||
|
"query"
|
||||||
|
],
|
||||||
|
"stop_navigation": [],
|
||||||
|
"open_app": [
|
||||||
|
"app"
|
||||||
|
],
|
||||||
|
"open_url": [
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"media": [
|
||||||
|
"command"
|
||||||
|
],
|
||||||
|
"torch": [
|
||||||
|
"on"
|
||||||
|
],
|
||||||
|
"settings_panel": [
|
||||||
|
"panel"
|
||||||
|
],
|
||||||
|
"automation_intent": [
|
||||||
|
"task"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"statuses": [
|
||||||
|
"done",
|
||||||
|
"declined",
|
||||||
|
"failed"
|
||||||
|
],
|
||||||
|
"wait_seconds": 300,
|
||||||
|
"message_max_chars": 1000,
|
||||||
|
"texts": {
|
||||||
|
"done": "Done on the phone.",
|
||||||
|
"declined": "The person declined it on the phone: nothing was done. Do not try again unless they ask.",
|
||||||
|
"failed": "The phone could not do it: nothing was done.",
|
||||||
|
"no_answer": "The phone did not answer in time: nothing was done.",
|
||||||
|
"says": "The phone says: {{message}}",
|
||||||
|
"unknown_action": "\"{{action}}\" is not an action. Use one of: {{actions}}. Nothing was sent to the phone.",
|
||||||
|
"missing": "{{action}} needs {{fields}}. Nothing was sent to the phone.",
|
||||||
|
"bad": "{{field}} is not {{what}}. Nothing was sent to the phone."
|
||||||
|
},
|
||||||
|
"what": {
|
||||||
|
"time": "a time as HH:MM (24-hour)",
|
||||||
|
"days": "a list of days from mon, tue, wed, thu, fri, sat, sun",
|
||||||
|
"seconds": "a whole number of seconds from 1 to 86400",
|
||||||
|
"start": "a date and time as YYYY-MM-DDTHH:MM, or a date as YYYY-MM-DD for a whole day",
|
||||||
|
"end": "a date and time as YYYY-MM-DDTHH:MM, or a date as YYYY-MM-DD for a whole day",
|
||||||
|
"to": "a list of recipients",
|
||||||
|
"cc": "a list of recipients",
|
||||||
|
"number": "a phone number",
|
||||||
|
"destination": "a place: lat and lon together, or a query, or a name",
|
||||||
|
"stop": "a place: lat and lon together, or a query, or a name",
|
||||||
|
"stops": "a list of places, each lat and lon together, or a query, or a name",
|
||||||
|
"mode": "one of drive, walk, bike, transit",
|
||||||
|
"near": "one of me, route",
|
||||||
|
"url": "an http or https address",
|
||||||
|
"command": "one of play, pause, toggle, next, previous",
|
||||||
|
"on": "true or false",
|
||||||
|
"panel": "one of wifi, bluetooth, internet, volume, nfc, location, do_not_disturb",
|
||||||
|
"values": "named text values"
|
||||||
|
},
|
||||||
|
"check": [
|
||||||
|
"time",
|
||||||
|
"days",
|
||||||
|
"seconds",
|
||||||
|
"start",
|
||||||
|
"end",
|
||||||
|
"to",
|
||||||
|
"cc",
|
||||||
|
"number",
|
||||||
|
"destination",
|
||||||
|
"stop",
|
||||||
|
"stops",
|
||||||
|
"mode",
|
||||||
|
"near",
|
||||||
|
"url",
|
||||||
|
"command",
|
||||||
|
"on",
|
||||||
|
"panel",
|
||||||
|
"values"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,22 +12,28 @@
|
|||||||
"max_output": { "type": "integer", "minimum": 1, "description": "The most tokens one reply may have." },
|
"max_output": { "type": "integer", "minimum": 1, "description": "The most tokens one reply may have." },
|
||||||
"temperature": { "type": "number", "minimum": 0, "maximum": 2, "description": "The sampling temperature the model is set up with. LLeMbas: the administrator's, which it also applies to a request that does not set one." },
|
"temperature": { "type": "number", "minimum": 0, "maximum": 2, "description": "The sampling temperature the model is set up with. LLeMbas: the administrator's, which it also applies to a request that does not set one." },
|
||||||
"top_p": { "type": "number", "minimum": 0, "maximum": 1, "description": "Nucleus sampling, as temperature." },
|
"top_p": { "type": "number", "minimum": 0, "maximum": 1, "description": "Nucleus sampling, as temperature." },
|
||||||
"efforts": { "type": "array", "items": { "enum": ["minimal", "low", "medium", "high", "xhigh", "max"] }, "description": "The reasoning efforts the model takes, in offering order. Narrowed when a server refuses one (quirks.json effort-refused)." },
|
"efforts": { "type": "array", "items": { "enum": ["minimal", "low", "medium", "high", "xhigh", "max"] }, "description": "The reasoning efforts the model takes, in offering order. Narrowed when a server refuses one (quirks.json effort-refused). Absent: each project's fallback (the CLI's: low, medium, high); [] — the model takes none, and its one effort level is `default`." },
|
||||||
"effort": { "enum": ["minimal", "low", "medium", "high", "xhigh", "max", "off"], "description": "The effort a new conversation starts with; off sends none." },
|
"thinking_switch": { "type": "boolean", "description": "Spec 3.2.0: the model's server turns thinking off for a request that sends `chat_template_kwargs.enable_thinking: false` (Qwen3.x and Nemotron 3.x templates on llama.cpp, vLLM, SGLang). Only then is the level `off` offered. Absent: false." },
|
||||||
|
"effort": { "enum": ["minimal", "low", "medium", "high", "xhigh", "max", "off", "default"], "description": "The level a new conversation starts with, when it is one of the levels the model offers (see `levels`); otherwise `medium` when the model takes it, else its first effort, else `default`. `off` — thinking switched off — exists only with thinking_switch; `default` — no effort sent, the model's own — only for a model that takes no efforts." },
|
||||||
"effort_style": { "enum": ["top", "kwargs", "both"], "description": "OpenAI Chat dialect: where the effort goes — top-level `reasoning_effort`, `chat_template_kwargs.reasoning_effort` (all llama.cpp reads), or both (the default)." },
|
"effort_style": { "enum": ["top", "kwargs", "both"], "description": "OpenAI Chat dialect: where the effort goes — top-level `reasoning_effort`, `chat_template_kwargs.reasoning_effort` (all llama.cpp reads), or both (the default)." },
|
||||||
"vision": { "type": "boolean", "description": "Takes images. Never sent images otherwise: most servers refuse the whole request." },
|
"vision": { "type": "boolean", "description": "Takes images. Never sent images otherwise: most servers refuse the whole request." },
|
||||||
"tools": { "type": "boolean", "description": "Takes tool calls. false: it can only talk; never sent a tools array." },
|
"tools": { "type": "boolean", "description": "Takes tool calls. false: it can only talk; never sent a tools array." },
|
||||||
"notes": { "type": "string", "maxLength": 300, "description": "What the model is good at, in a line: other models are told about it in the roster, for handing work over." },
|
"notes": { "type": "string", "maxLength": 300, "description": "What the model is good at, in a line: other models are told about it in the roster, for handing work over." },
|
||||||
"capacity": {
|
"capacity": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "How much the model's server can do at once, so a helper or a subagent on it is not started where it would evict or wait for the model already working.",
|
"description": "How much the model's server can do at once, so a subagent on it is not started where it would evict or wait for the model already working (subagents.md §3).",
|
||||||
"properties": {
|
"properties": {
|
||||||
"group": { "type": "string", "description": "Models that share one server holding one model at a time (llama-swap): starting another model of the group unloads this one. The CLI: a model's `group`, or its connection when the connection sets one_model_at_a_time. LLeMbas: the connection's name when it sets one_model_at_a_time, which is what /v1/models says." },
|
"group": { "type": "string", "description": "Models that share one server holding one model at a time (llama-swap): starting another model of the group unloads this one, so a subagent on another model of the session's model's group is refused (spec 3.3.0, subagents.md §3). The CLI: a model's `group`, or its connection when the connection sets one_model_at_a_time. LLeMbas: the connection's name when it sets one_model_at_a_time, which is what /v1/models says." },
|
||||||
"single_session": { "type": "boolean", "description": "The model serves one request at a time: a second one waits behind the first." }
|
"single_session": { "type": "boolean", "description": "The model serves one request at a time: a second one waits behind the first. The same as `parallel: 1`, which a server that knows `parallel` sends beside it for older clients." },
|
||||||
|
"parallel": { "type": "integer", "minimum": 1, "description": "Spec 3.2.0: how many requests the model's server serves at once (llama.cpp's slots, a vLLM or SGLang batch); one more waits behind them. Read as: `parallel` when given, else 1 when single_session is true, else no limit (a hosted API). Where both are given and disagree, `parallel` wins. The session's model is its own subagents' model only when it serves at least 2; a one-request session needs a named model on another server (spec 3.3.0, subagents.md §3, §4)." }
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"levels": {
|
||||||
|
"about": "Spec 3.2.0. The effort levels a chat offers for a model, in order: `off` when thinking_switch is true, then the model's `efforts` — or, when it takes none, the one level `default`. What each sends (OpenAI chat): `off` — `chat_template_kwargs.enable_thinking: false` and no `reasoning_effort` anywhere; `default` — neither; an effort — `reasoning_effort` where effort_style puts it, and no `enable_thinking`. The Anthropic dialect: `off` sends no `thinking`. A level the model does not offer (an `off` for a model without the switch, from an older client or a stored chat) is read as the start level (`effort`'s rule). How it is shown: one level — no effort control at all; two, one of them `off` — a Thinking on/off switch, on being the other level; more — a slider or a list, in this order. conformance/levels.json.",
|
||||||
|
"control": ["none", "switch", "slider"]
|
||||||
|
},
|
||||||
"required": ["id"],
|
"required": ["id"],
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
"glob": "allow",
|
"glob": "allow",
|
||||||
"grep": "allow",
|
"grep": "allow",
|
||||||
"list": "allow",
|
"list": "allow",
|
||||||
|
"search": "allow",
|
||||||
|
"recall": "allow",
|
||||||
|
"show": "allow",
|
||||||
|
"python": "allow",
|
||||||
"ask_user": "allow",
|
"ask_user": "allow",
|
||||||
"plan_submit": "allow",
|
"plan_submit": "allow",
|
||||||
"task": "allow",
|
"task": "allow",
|
||||||
@@ -22,6 +26,17 @@
|
|||||||
"job_stop": "allow",
|
"job_stop": "allow",
|
||||||
"job_send": "allow",
|
"job_send": "allow",
|
||||||
"report": "allow",
|
"report": "allow",
|
||||||
|
"team_add": "allow",
|
||||||
|
"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",
|
"web_search": "allow",
|
||||||
"memory": "allow",
|
"memory": "allow",
|
||||||
"session_search": "allow",
|
"session_search": "allow",
|
||||||
@@ -74,6 +89,12 @@
|
|||||||
"tree *-o*": "ask",
|
"tree *-o*": "ask",
|
||||||
"tree *-R*": "ask",
|
"tree *-R*": "ask",
|
||||||
"file *-C*": "ask"
|
"file *-C*": "ask"
|
||||||
}
|
},
|
||||||
|
"commit": "ask",
|
||||||
|
"branch": "ask",
|
||||||
|
"pr": "ask",
|
||||||
|
"merge": "ask",
|
||||||
|
"tag": "ask",
|
||||||
|
"release": "ask"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"order_about": "Loosest first. A project or a subagent may make the mode stricter, never looser; switching to a looser mode is always asked.",
|
"order_about": "Loosest first. A project or a subagent may make the mode stricter, never looser; switching to a looser mode is always asked.",
|
||||||
"aliases": { "unrestricted": "auto" },
|
"aliases": { "unrestricted": "auto" },
|
||||||
"floor": "The hardline (hardline.json) and the protected paths are refused in every mode, auto included.",
|
"floor": "The hardline (hardline.json) and the protected paths are refused in every mode, auto included.",
|
||||||
|
"confirm": "Spec 3.0.0: a tool with `confirm: always` (merge, tag, release) is put to the person before it runs, in every mode, auto included, as an approval card naming the call and what it reaches; it runs on a yes only. No rule allows it and no approval is remembered (an approval card offers once, never always). With nobody to answer, it is refused, as unattended says.",
|
||||||
"modes": {
|
"modes": {
|
||||||
"manual": {
|
"manual": {
|
||||||
"summary": "Everything not already allowed is asked.",
|
"summary": "Everything not already allowed is asked.",
|
||||||
@@ -31,7 +32,8 @@
|
|||||||
"read": "rules",
|
"read": "rules",
|
||||||
"write": "allow only for files under the plans directory; anything else is refused",
|
"write": "allow only for files under the plans directory; anything else is refused",
|
||||||
"execute": "only what the rules already allow; anything else is refused",
|
"execute": "only what the rules already allow; anything else is refused",
|
||||||
"interact": "rules"
|
"interact": "rules",
|
||||||
|
"refused": "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."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"unattended": "With nobody to answer, whatever a mode would ask is refused instead, and the model is told so up front (prompts/modes/unattended.md)."
|
"unattended": "With nobody to answer, whatever a mode would ask is refused instead, and the model is told so up front (prompts/modes/unattended.md)."
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Phone actions
|
||||||
|
|
||||||
|
Spec 3.6.0. `phone_action` (`tools/phone_action.json`) lets a chat's model do something on the
|
||||||
|
person's phone — set an alarm, start a route, draft a message — when the person is writing from the
|
||||||
|
LLeMbas app on that phone. The model proposes; **the app shows the person a card and does nothing
|
||||||
|
until they confirm it there**; the app carries it out with the phone's own apps and answers what
|
||||||
|
happened, which is the call's result. The texts and numbers are `loop.json` `phone`.
|
||||||
|
|
||||||
|
It is the server's tool alone (`scope: llembas`, `sessions: chat`). The CLI never offers it: a
|
||||||
|
terminal has no phone behind it.
|
||||||
|
|
||||||
|
## 1. Who offers it
|
||||||
|
|
||||||
|
- **The capability.** The app declares `phone_actions` (`loop.json` `phone.capability`) with each
|
||||||
|
message it sends. A message sent from anywhere else — the web, Desktop, another app — declares
|
||||||
|
nothing.
|
||||||
|
- **`requires: phone`.** The tool is offered in a chat once a message has been sent to it with the
|
||||||
|
capability, and stays offered for the rest of the chat (the tool list is the session's,
|
||||||
|
`context.md` §10): a chat started on the phone and continued on the web keeps the tool, and a web
|
||||||
|
chat continued on the phone gains it from that message on (the list changes once, so the prompt
|
||||||
|
is made again once).
|
||||||
|
- **Unavailable.** A call answers the tool's `unavailable` text, word for word, when the message
|
||||||
|
being answered was not sent with the capability (there is no phone to confirm on), and in a chat
|
||||||
|
nobody attends (a schedule's, a subagent's). Nothing reaches any phone then.
|
||||||
|
- **Confirm: always.** Every call is put to the person, in every mode; nothing remembers an answer.
|
||||||
|
The card is the phone's own: the server never asks a second time on another screen. Its risk is
|
||||||
|
`interact`: no mode refuses it, Plan mode included, because nothing on disk changes and the
|
||||||
|
person decides on the phone.
|
||||||
|
|
||||||
|
## 2. A call
|
||||||
|
|
||||||
|
`action` is one of `phone.actions`; `summary` is the line the phone shows. Each action reads only
|
||||||
|
its `phone.fields`; anything else in the call is dropped before the phone sees it. The call is
|
||||||
|
checked before anything is sent, in this order, and the first problem is the call's result:
|
||||||
|
|
||||||
|
1. `action` not one of the actions: `texts.unknown_action` (`{{actions}}` the list, joined with
|
||||||
|
`, `).
|
||||||
|
2. The action's `phone.required`, each entry in turn: a field is present when it is not null, not
|
||||||
|
an empty string (after trimming) and not an empty list or object; a list inside the list is
|
||||||
|
present when any one of its fields is. Every entry that is missing, in order, goes in one
|
||||||
|
`texts.missing`: a field by its name, a group as its names joined with ` or `, the entries
|
||||||
|
joined with ` and ` (`add_stop needs stop and destination.`).
|
||||||
|
3. The fields present, in `phone.check` order (only those the action reads): one that is not what
|
||||||
|
it should be is `texts.bad` with `{{field}}` its name and `{{what}}` from `phone.what`:
|
||||||
|
- `time`: `HH:MM`, hours 00–23, minutes 00–59.
|
||||||
|
- `days`: a list, each one of `mon` … `sun`.
|
||||||
|
- `seconds`: an integer (never a boolean) from 1 to 86400.
|
||||||
|
- `start`, `end`: `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM` (not checked as a calendar date).
|
||||||
|
- `to`, `cc`: a list of strings, each non-empty after trimming.
|
||||||
|
- `number`: at least three digits, and nothing but digits, spaces and `+ ( ) - . / # *`.
|
||||||
|
- `destination`, `stop`: an object with numeric `lat` and `lon` together (each in its range),
|
||||||
|
or a non-empty `query`, or a non-empty `name`. `stops`: a list of such places.
|
||||||
|
- `mode`, `near`, `command`, `panel`: one of the schema's values.
|
||||||
|
- `url`: begins `http://` or `https://` (any case) and has something after it.
|
||||||
|
- `on`: a boolean.
|
||||||
|
- `values`: an object of strings.
|
||||||
|
|
||||||
|
A call that passes is the **action** the phone receives: `action`, `summary` (trimmed; the action's
|
||||||
|
name when empty) and the action's fields that are present, as the model gave them, strings trimmed.
|
||||||
|
|
||||||
|
## 3. The answer
|
||||||
|
|
||||||
|
The server waits up to `phone.wait_seconds` for the phone's answer: `status` (`done`, `declined`
|
||||||
|
or `failed`) and an optional `message`, at most `phone.message_max_chars` characters (cut there).
|
||||||
|
The call's result is the status's text — `texts.done`, `texts.declined`, `texts.failed` — and,
|
||||||
|
when there is a message, a blank line and `texts.says` with it. A status the phone does not know is
|
||||||
|
`failed`. No answer in time is `texts.no_answer`, with no message; the card goes away.
|
||||||
|
|
||||||
|
What the person pressed is theirs: **declined** means they said no, and the model is told not to
|
||||||
|
try again unless asked. **Failed** is the phone's: an app missing, a permission refused, a map app
|
||||||
|
that cannot do what was asked.
|
||||||
|
|
||||||
|
## 4. Navigation
|
||||||
|
|
||||||
|
Map apps differ, and the phone says when one cannot: the model is not told which app the person
|
||||||
|
uses.
|
||||||
|
|
||||||
|
- `navigate` starts a route to `destination` through `stops`, in order; `mode` is how the person
|
||||||
|
travels (`drive` when not said). `replace_route` replaces a route already running without the
|
||||||
|
phone asking first.
|
||||||
|
- `add_stop`: **no map app adds a stop to a running route**, so the phone plans again — from where
|
||||||
|
the person is, to `stop`, then to `destination`. The model gives `destination` as the person told
|
||||||
|
it when the route began: the tool's description tells it to keep it.
|
||||||
|
- `search_nearby` looks for `query` around the person (`near: me`) or along the running route
|
||||||
|
(`near: route`).
|
||||||
|
- `stop_navigation` ends the route where the map app allows it; one that does not answers
|
||||||
|
`failed` with why.
|
||||||
|
|
||||||
|
## 5. The other actions
|
||||||
|
|
||||||
|
- `alarm` and `timer` are the phone's clock app; `days` repeats an alarm.
|
||||||
|
- `calendar_event` opens the calendar's new-event screen filled in; the person saves it.
|
||||||
|
- `sms_draft` and `email_draft` are **drafts**: the messaging or mail app opens with them, and the
|
||||||
|
person sends. `dial` opens the dialer with the number; the person places the call.
|
||||||
|
- `open_app` names an app as the person does; `open_url` opens an address in the browser.
|
||||||
|
- `media` controls what is playing; `torch` switches the torch.
|
||||||
|
- `settings_panel` opens the phone's quick panel or page for a setting; the person changes it.
|
||||||
|
- `automation_intent` runs an automation the person made in an automation app (Tasker when `app`
|
||||||
|
is not said), by its name, with `values` as its inputs.
|
||||||
@@ -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.<phase>` (and `progress.<phase>.<variant>`, `progress.reason.<reason>`,
|
||||||
|
`progress.what.<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.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
kept by the agent, and how git is used there is the user's answer, asked once and written down. -->
|
kept by the agent, and how git is used there is the user's answer, asked once and written down. -->
|
||||||
## AGENTS.md
|
## AGENTS.md
|
||||||
This project has no AGENTS.md yet. It is the file every later session here starts from, so making it is part of the work:
|
This project has no AGENTS.md yet. It is the file every later session here starts from, so making it is part of the work:
|
||||||
- Make it at the first natural pause in real work, once you know what the project is — not before you have done anything, and not in plan mode. It goes at the project root: {{root}}/AGENTS.md.
|
- Make it at the first natural pause in real work, once you know what the project is — not before you have done anything, and not in plan mode. It goes at the project root: `AGENTS.md`.
|
||||||
- Before you write it, ask the user how git is to be used here, with ask_user, in one call: {{git_question}} Mark the options you would recommend.
|
- Before you write it, ask the user how git is to be used here, with ask_user, in one call: {{git_question}} Mark the options you would recommend.
|
||||||
- Write their answer under a "## Git" heading. Above it, say briefly what the project is, how to build, run and test it, and the conventions that matter.
|
- Write their answer under a "## Git" heading. Above it, say briefly what the project is, how to build, run and test it, and the conventions that matter.
|
||||||
- If the user does not want the file, do not bring it up again this session.
|
- If the user does not want the file, do not bring it up again this session.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<!-- LLeMbas CLI and LLeMbas, spec 3.0.0: how tool results are kept short (context.md). -->
|
||||||
|
# Tool results
|
||||||
|
- Every tool result starts with its id in brackets, like `[#0012]`. A read's bracket also says the path, the lines shown, the file's length and a short hash of its content: `[#0012 src/app.ts lines 1-200 of 340 sha 1c9e04ab]`.
|
||||||
|
- Reading a file that has not changed since an earlier read in this conversation returns a stub instead of the lines: `Unchanged since #0012, which already shows lines 1-200.` The content is in that earlier result. Use it; do not read the file again to see it.
|
||||||
|
- A listing or search repeated with the same result returns `Same result as #0026.` in the same way.
|
||||||
|
- Long output is shortened to its beginning and its end, with `[1234 lines elided, full output #0020]` between them.
|
||||||
|
- `recall` with an id shows that stored result in full, or from a line on: for lines left out of a shortened output, or for a result from before the conversation was summarised. Nothing is lost; recall only what you need.
|
||||||
|
- An edit answers with the diff of the change, not the whole file. The earlier read with that diff applied is the file now: read it again only for lines you have not seen.
|
||||||
|
- After the conversation is summarised, earlier results are no longer in front of you. The summary ends with a table of the files read before it, with their ids; a file read again then comes back in full.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- LLeMbas CLI: the subagent's standing, said once at the end of its system prompt in place of
|
<!-- LLeMbas CLI: the subagent's standing, said once at the end of its system prompt in place of
|
||||||
the mode text; LLeMbas's helper says the same. The variables are what differs per run. -->
|
the mode text; LLeMbas's subagent says the same. The variables are what differs per run. -->
|
||||||
You are the "{{name}}" subagent. You work for the main agent, not for the user directly, on one task it gave you; you cannot ask the user anything. {{instructions}}
|
You are the "{{name}}" subagent. You work for the main agent, not for the user directly, on one task it gave you; you cannot ask the user anything. {{instructions}}
|
||||||
|
|
||||||
Your final reply is your report to the main agent — make it complete on its own: the result, then the evidence (file:line), then anything you could not settle.{{report}}{{read_only}}
|
Your final reply is your report to the main agent — make it complete on its own: the result, then the evidence (file:line), then anything you could not settle.{{report}}{{read_only}}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!-- LLeMbas CLI and LLeMbas, spec 3.2.0 (team.md), 3.4.0 (subagents.md §5–§13): how the main agent
|
||||||
|
leads its subagents, in the system prompt whenever the team tools are offered. Constant: the
|
||||||
|
size the person allows comes in a session note, never here. -->
|
||||||
|
# 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.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!-- LLeMbas CLI and LLeMbas, spec 3.2.0 (team.md), 3.4.0 (subagents.md §13): a subagent's standing,
|
||||||
|
said once at the end of its system prompt in place of blocks/subagent.md. The variables are what
|
||||||
|
differs per subagent; each stands on a line of its own, so no punctuation of theirs matters. -->
|
||||||
|
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 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 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}}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<!-- LLeMbas CLI and LLeMbas, spec 3.4.0 (subagents.md §5, §13): what a sub-manager is told, in
|
||||||
|
place of the whole of blocks/team_lead.md that a member with `lead: true` got in 3.2.0. -->
|
||||||
|
# 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.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<!-- LLeMbas, spec 3.0.0: the file system a chat works in (filesystem.md). -->
|
||||||
|
# Files
|
||||||
|
- You work in the user's file system, through the file tools. Relative paths start at {{start}}; absolute paths start at `/`.
|
||||||
|
- What this session can reach, and how: {{view}}. Nothing else exists here. A path below one of these is reached by its full path; a directory above one that is not itself listed cannot be listed, read or written.
|
||||||
|
- A listing ends a directory with `/`, marks a repository mounted from a forge `[mount]`, and an entry you can read but not change `[read-only]`.
|
||||||
|
- Read-only means read but not changed. Stage means changes are kept in this session and, in a repository, not committed. Commit means that, in a repository, the changes may also be committed and pushed to the forge.
|
||||||
|
- {{repositories}}
|
||||||
|
- Deleting moves a file to the trash, from which the user can restore it. Still, delete only what you were asked to, or what you made yourself.
|
||||||
|
- {{show}}
|
||||||
|
- {{python}}
|
||||||
+54
-13
@@ -11,7 +11,7 @@
|
|||||||
"intro": "the opening line: what the model is here to help with on this surface (the CLI: software engineering and running the project; LLeMbas: whatever the person is working on)",
|
"intro": "the opening line: what the model is here to help with on this surface (the CLI: software engineering and running the project; LLeMbas: whatever the person is working on)",
|
||||||
"surface": "where the output is shown and how it is rendered (a terminal, monospace, GitHub-flavoured markdown; a web page rendering markdown)",
|
"surface": "where the output is shown and how it is rendered (a terminal, monospace, GitHub-flavoured markdown; a web page rendering markdown)",
|
||||||
"tool_list": "LLeMbas: the exact tools offered on this request, as a sentence; the CLI: empty",
|
"tool_list": "LLeMbas: the exact tools offered on this request, as a sentence; the CLI: empty",
|
||||||
"file_tools": "the CLI: the bullet about using read, edit, write, grep and glob rather than shell commands; LLeMbas: empty",
|
"file_tools": "the bullet about using the dedicated file tools — read, edit, write, grep, glob — rather than shell commands: the CLI's, and since spec 3.0.0 LLeMbas's in a chat as well (where there is no shell, the bullet says the file tools are the way to the files); empty for a session without file tools",
|
||||||
"plan_tools": "the bullet about planning with plan_write from the start and keeping the plan current with plan_update, and saying so with ask_user when the plan turns out wrong; empty for a subagent, which has no plan tools",
|
"plan_tools": "the bullet about planning with plan_write from the start and keeping the plan current with plan_update, and saying so with ask_user when the plan turns out wrong; empty for a subagent, which has no plan tools",
|
||||||
"plan_context": "the current plan, rendered with its ids, under a heading, with the sentence to work to it and quote the ids; empty when there is none",
|
"plan_context": "the current plan, rendered with its ids, under a heading, with the sentence to work to it and quote the ids; empty when there is none",
|
||||||
"ask_tool": "the bullet about asking with ask_user when a decision is the user's; empty for a subagent, which has nobody to ask",
|
"ask_tool": "the bullet about asking with ask_user when a decision is the user's; empty for a subagent, which has nobody to ask",
|
||||||
@@ -183,25 +183,21 @@
|
|||||||
"llembas_text": "identical"
|
"llembas_text": "identical"
|
||||||
},
|
},
|
||||||
"blocks/prototypes.md": {
|
"blocks/prototypes.md": {
|
||||||
"scope": "shared",
|
"scope": "cli",
|
||||||
"llembas": [
|
"llembas": [],
|
||||||
"core.prototypes"
|
|
||||||
],
|
|
||||||
"llembas_text": "identical",
|
|
||||||
"variables": {
|
"variables": {
|
||||||
"prototype_sources": "whether the page may load anything over https (the instance's network setting), as one sentence",
|
"prototype_sources": "whether the page may load anything over https (the instance's network setting), as one sentence",
|
||||||
"prototype_surface": "where it is seen on this surface: empty in the web UI, which renders it; in a terminal, that it is saved as a file the person can open, and that the web UI renders it"
|
"prototype_surface": "where it is seen on this surface: empty in the web UI, which renders it; in a terminal, that it is saved as a file the person can open, and that the web UI renders it"
|
||||||
}
|
},
|
||||||
|
"why": "spec 3.0.0: a chat has no prototypes (show replaces them); the CLI keeps the text while a client still offers them over the link"
|
||||||
},
|
},
|
||||||
"blocks/library.md": {
|
"blocks/library.md": {
|
||||||
"scope": "shared",
|
"scope": "cli",
|
||||||
"llembas": [
|
"llembas": [],
|
||||||
"core.library"
|
|
||||||
],
|
|
||||||
"llembas_text": "identical",
|
|
||||||
"variables": {
|
"variables": {
|
||||||
"knowledge_scope": "the sentence naming the knowledge bases this conversation is limited to (\"Knowledge searches here cover only: …. Finding nothing there means nothing is there, not that the library is empty.\"), or empty"
|
"knowledge_scope": "the sentence naming the knowledge bases this conversation is limited to (\"Knowledge searches here cover only: …. Finding nothing there means nothing is there, not that the library is empty.\"), or empty"
|
||||||
}
|
},
|
||||||
|
"why": "spec 3.0.0: a chat has no knowledge bases (its files are searched with search); the CLI keeps its own local library"
|
||||||
},
|
},
|
||||||
"blocks/subagent.md": {
|
"blocks/subagent.md": {
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
@@ -227,6 +223,51 @@
|
|||||||
"llembas": [],
|
"llembas": [],
|
||||||
"llembas_text": "identical",
|
"llembas_text": "identical",
|
||||||
"why": "LLeMbas reads it from the vendored spec (harness_spec.prompt), not from a fragment"
|
"why": "LLeMbas reads it from the vendored spec (harness_spec.prompt), not from a fragment"
|
||||||
|
},
|
||||||
|
"blocks/context.md": {
|
||||||
|
"scope": "shared",
|
||||||
|
"llembas": [],
|
||||||
|
"llembas_text": "differs",
|
||||||
|
"why": "not adopted yet (spec 3.0.0)"
|
||||||
|
},
|
||||||
|
"blocks/workspace.md": {
|
||||||
|
"scope": "llembas",
|
||||||
|
"llembas": [],
|
||||||
|
"variables": {
|
||||||
|
"start": "the session's start directory, as a code span",
|
||||||
|
"view": "every path granted to this session with its level (read-only, stage, commit), as a list in one sentence, a mount named as a repository with its branch",
|
||||||
|
"repositories": "when the view holds a repository from a forge: the sentence saying changes there collect in this session's working copy until `commit`, that a branch moved on the forge refuses the commit and shows the difference, and that merge, tag and release always ask the user; otherwise empty",
|
||||||
|
"show": "when the session has a screen (`show` is offered): the sentence saying to open a file the user should see with show — a page, a chart, a document you made — rather than pasting it; otherwise empty",
|
||||||
|
"python": "when `python` is offered: the sentence saying quick calculations and file work can be done in Python in the user's browser, and that it cannot run when no browser page is open; otherwise empty"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blocks/team_lead.md": {
|
||||||
|
"scope": "shared",
|
||||||
|
"llembas": [],
|
||||||
|
"llembas_text": "identical",
|
||||||
|
"why": "spec 3.2.0: LLeMbas reads it from the vendored spec (harness_spec.prompt), not from a fragment"
|
||||||
|
},
|
||||||
|
"blocks/team_member.md": {
|
||||||
|
"scope": "shared",
|
||||||
|
"llembas": [],
|
||||||
|
"llembas_text": "identical",
|
||||||
|
"why": "spec 3.2.0, 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 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/<name>.md for a built-in one), as in blocks/subagent.md",
|
||||||
|
"read_only": "in plan mode: the sentence saying it can read and search but change nothing (as in blocks/subagent.md); otherwise empty",
|
||||||
|
"lead": "for a 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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,4 @@ Leave out pleasantries, retracted ideas and anything already superseded. Do not
|
|||||||
|
|
||||||
{{previous_summary}}
|
{{previous_summary}}
|
||||||
|
|
||||||
## Transcript
|
|
||||||
|
|
||||||
{{transcript}}
|
{{transcript}}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<!-- Source: OpenCode command/template/initialize.txt (MIT, © 2025 opencode), shortened, and made to
|
<!-- Source: OpenCode command/template/initialize.txt (MIT, © 2025 opencode), shortened, and made to
|
||||||
ask about git the way blocks/agents-missing.md does (LLeMbas CLI's /init). -->
|
ask about git the way blocks/agents-missing.md does (LLeMbas CLI's /init). -->
|
||||||
Create or update `AGENTS.md` at the project root, {{root}}/AGENTS.md: the file every later session here starts from. Every line should answer "would an agent likely get this wrong without being told?" — if not, leave it out.
|
Create or update `AGENTS.md` at the project root: the file every later session here starts from. Every line should answer "would an agent likely get this wrong without being told?" — if not, leave it out.
|
||||||
|
|
||||||
{{focus}}
|
{{focus}}
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -2,16 +2,17 @@
|
|||||||
"about": "What OpenAI-compatible servers really do, and how both harnesses cope. Each quirk is detected, handled, and — where marked — learned, so the next request does not trip it again. The CLI keeps what it learned in ~/.local/state/lembas/learned.json; LLeMbas on the model row or in process memory.",
|
"about": "What OpenAI-compatible servers really do, and how both harnesses cope. Each quirk is detected, handled, and — where marked — learned, so the next request does not trip it again. The CLI keeps what it learned in ~/.local/state/lembas/learned.json; LLeMbas on the model row or in process memory.",
|
||||||
"quirks": [
|
"quirks": [
|
||||||
{ "id": "stream-options-refused", "servers": "older llama.cpp, some proxies", "symptom": "400 or 422 to a request carrying stream_options", "handling": "retry once without it, before anything was streamed", "learned": true },
|
{ "id": "stream-options-refused", "servers": "older llama.cpp, some proxies", "symptom": "400 or 422 to a request carrying stream_options", "handling": "retry once without it, before anything was streamed", "learned": true },
|
||||||
{ "id": "effort-refused", "servers": "llama.cpp chat templates (Qwen3.8 refuses high)", "symptom": "an error whose text names the effort and says unexpected or supported (conformance/effort.json)", "handling": "retry once without an effort, before anything was streamed; narrow the model's efforts to those the message advertises", "learned": true },
|
{ "id": "effort-refused", "servers": "llama.cpp chat templates (Qwen3.8 refuses high)", "symptom": "an error whose text names the effort and says unexpected or supported (conformance/effort.json)", "handling": "retry once without an effort (and without enable_thinking), before anything was streamed; narrow the model's efforts to those the message advertises. A narrowed list keeps the level off when the model has thinking_switch (models/schema.json levels)", "learned": true },
|
||||||
{ "id": "effort-placement", "servers": "llama.cpp", "symptom": "a top-level reasoning_effort is silently dropped", "handling": "send it top-level and as chat_template_kwargs.reasoning_effort (effort_style both)", "learned": false },
|
{ "id": "effort-placement", "servers": "llama.cpp", "symptom": "a top-level reasoning_effort is silently dropped", "handling": "send it top-level and as chat_template_kwargs.reasoning_effort (effort_style both)", "learned": false },
|
||||||
{ "id": "prompt-progress", "servers": "llama.cpp with return_progress", "symptom": "prompt_progress {total, cache, processed, time_ms} frames before the reply", "handling": "ask for it, show how far the prompt has been read; a 400 to the field is learned and it is not asked again", "learned": true },
|
{ "id": "prompt-progress", "servers": "llama.cpp with return_progress", "symptom": "prompt_progress {total, cache, processed, time_ms} frames before the reply", "handling": "ask for it, show how far the prompt has been read; a 400 to the field is learned and it is not asked again", "learned": true },
|
||||||
{ "id": "reasoning-fields", "servers": "llama.cpp, vLLM, DeepSeek, OpenRouter", "symptom": "reasoning arrives as reasoning_content, reasoning, or inline <think>/<thinking>/<reasoning> tags (conformance/think.json)", "handling": "all three read as reasoning; reasoning is never sent back as context", "learned": false },
|
{ "id": "reasoning-fields", "servers": "llama.cpp, vLLM, DeepSeek, OpenRouter", "symptom": "reasoning arrives as reasoning_content, reasoning, or inline <think>/<thinking>/<reasoning> tags (conformance/think.json)", "handling": "all three read as reasoning. From spec 3.2.0 an assistant message of the current turn carries its reasoning back as reasoning_content, and earlier turns' carry none (context.md §8); inline tags are never sent back as text", "learned": false },
|
||||||
{ "id": "error-in-200-stream", "servers": "llama.cpp", "symptom": "an error object inside a 200 event stream, sometimes as an unterminated last line", "handling": "read as the error it is, not as an empty reply", "learned": false },
|
{ "id": "error-in-200-stream", "servers": "llama.cpp", "symptom": "an error object inside a 200 event stream, sometimes as an unterminated last line", "handling": "read as the error it is, not as an empty reply", "learned": false },
|
||||||
{ "id": "tool-call-without-index", "servers": "some llama.cpp builds", "symptom": "tool-call fragments with no index", "handling": "fragments without an index continue the last call", "learned": false },
|
{ "id": "tool-call-without-index", "servers": "some llama.cpp builds", "symptom": "tool-call fragments with no index", "handling": "fragments without an index continue the last call", "learned": false },
|
||||||
{ "id": "swap-banner", "servers": "llama-swap", "symptom": "a loading banner streamed before the reply while the model loads", "handling": "not shown as the reply", "learned": false },
|
{ "id": "swap-banner", "servers": "llama-swap", "symptom": "a loading banner streamed before the reply while the model loads", "handling": "not shown as the reply", "learned": false },
|
||||||
{ "id": "model-unloaded", "servers": "llama-swap", "symptom": "a 'model unloaded' frame mid-reply, when another client loaded a different model", "handling": "treated as a server error: retried once", "learned": false },
|
{ "id": "model-unloaded", "servers": "llama-swap", "symptom": "a 'model unloaded' frame mid-reply, when another client loaded a different model", "handling": "treated as a server error: retried once", "learned": false },
|
||||||
{ "id": "zero-usage", "servers": "several", "symptom": "a usage block of all zeros", "handling": "ignored; the count is estimated (marked ~) instead", "learned": false },
|
{ "id": "zero-usage", "servers": "several", "symptom": "a usage block of all zeros", "handling": "ignored; the count is estimated (marked ~) instead", "learned": false },
|
||||||
{ "id": "no-context-length", "servers": "llama-swap /v1/models, most OpenAI-compatible servers", "symptom": "/models lists ids without a context length", "handling": "ask llama-server's /props (through llama-swap's /upstream/<model>), Ollama's /api/show, else unknown — never assume one", "learned": true },
|
{ "id": "no-context-length", "servers": "llama-swap /v1/models, most OpenAI-compatible servers", "symptom": "/models lists ids without a context length", "handling": "ask llama-server's /props (through llama-swap's /upstream/<model>), Ollama's /api/show, else unknown — never assume one", "learned": true },
|
||||||
|
{ "id": "thinking-default", "servers": "llama.cpp, vLLM and SGLang serving Qwen3.x and Nemotron 3.x", "symptom": "a request that sends no effort still thinks, at the effort the server was started with — no effort is not no thinking", "handling": "thinking is switched off only by chat_template_kwargs.enable_thinking: false, on a model marked thinking_switch (models/schema.json levels); a request's own reasoning_effort overrides the server's default, and a value the template does not know is effort-refused", "learned": false },
|
||||||
{ "id": "max-completion-tokens", "servers": "OpenAI reasoning models", "symptom": "max_tokens refused", "handling": "send max_completion_tokens where configured", "learned": false }
|
{ "id": "max-completion-tokens", "servers": "OpenAI reasoning models", "symptom": "max_tokens refused", "handling": "send max_completion_tokens where configured", "learned": false }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
# Subagents
|
||||||
|
|
||||||
|
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`
|
||||||
|
`subagents`; the cases `conformance/subagents.json`.
|
||||||
|
|
||||||
|
## 1. The word
|
||||||
|
|
||||||
|
**Subagent** is the word in every text a person or a model reads: the UI (the web UI, Desktop,
|
||||||
|
Android, the CLI, the admin pages), the prompt texts and the tool texts. A team's members are
|
||||||
|
subagents too. The word *helper* is retired. Tool names, their former names (`aliases`) and
|
||||||
|
stored identifiers do not change.
|
||||||
|
|
||||||
|
## 2. The model a subagent runs on
|
||||||
|
|
||||||
|
**The session's own model**, always — unless **the person has named another model** in this
|
||||||
|
session. Nothing else chooses one: there is **no setting** for a subagent's model anywhere — not
|
||||||
|
in a chat, not in a person's or an instance's settings, not in the configuration, and an agent
|
||||||
|
definition's own `model` (the CLI's agent files) is not used for a subagent either.
|
||||||
|
|
||||||
|
- **Named** means: in a message **the person typed** in this session, the model's served id, its
|
||||||
|
id at its connection, or its display name occurs as a whole word, letters compared without case.
|
||||||
|
Only the person's own words count — never the model's text, a tool's output, a file, a note, a
|
||||||
|
job's or a team's marked message, or a subagent's report. A message that was compacted away
|
||||||
|
still counts: what the person named stays named for the whole session.
|
||||||
|
- **A whole word**: the character before the name is not a letter, a digit, `_`, `-`, `/`, `.` or
|
||||||
|
`:`; the character after it is not a letter, a digit, `_`, `-` or `/`, nor a `.` or `:` followed
|
||||||
|
by a letter or a digit. So `coder-30b.` at the end of a sentence names `coder-30b`, and
|
||||||
|
`coder-30b-instruct` does not. Runs of white space in a display name match any run of white
|
||||||
|
space.
|
||||||
|
- The `model` parameter of `task` and of `team_add` takes a model the person may use, by the same
|
||||||
|
names. When it is **not the session's model and the person has not named it**, the call is
|
||||||
|
refused, word for word, with `subagents.texts.not_named`:
|
||||||
|
|
||||||
|
> Use another model for subagents only when the person names it.
|
||||||
|
|
||||||
|
The session's own model given as `model` is the default spelled out, and is never refused for
|
||||||
|
not being named.
|
||||||
|
- A team's members are of the lead's session: what the person named there counts for every
|
||||||
|
`team_add` in the team, a sub-manager's included. A member's default is the session's model, not
|
||||||
|
its manager's.
|
||||||
|
|
||||||
|
## 3. Capacity: the logical restrictions, and no others
|
||||||
|
|
||||||
|
What a model's server can do at once is `models/schema.json` `capacity`: `parallel` (`single_session`
|
||||||
|
being `parallel: 1`; neither said — no limit), and `group`, the server that holds one model at a
|
||||||
|
time. With **S** the session's model and **M** the subagent's, checked in this order, after
|
||||||
|
`not_named`:
|
||||||
|
|
||||||
|
1. **M is another model on S's one-model server** (both have the same `group`): refused with
|
||||||
|
`subagents.texts.same_group` — starting it would unload the lead's model:
|
||||||
|
|
||||||
|
> That model is on the same one-model server as this chat's model; name a model on another server.
|
||||||
|
|
||||||
|
2. **M is S, and S serves one request at a time** (`parallel` 1): refused with
|
||||||
|
`subagents.texts.one_request` — the subagent would take the only slot from the lead:
|
||||||
|
|
||||||
|
> This model serves one request at a time: name another model, on another server, for subagents.
|
||||||
|
|
||||||
|
There is **no fallback**: nothing else is picked for the person, ever. A model the person named,
|
||||||
|
on another server, is the way out — and a model that serves one request at a time is fine there.
|
||||||
|
|
||||||
|
These apply to `task` and to `team_add` alike, and before anything is started. Beyond them nothing
|
||||||
|
is refused for being the wrong model: a member past the free slots waits for one (`team.md` §8),
|
||||||
|
and `task` keeps its rule for a model that is already full (`loop.json` `capacity`).
|
||||||
|
|
||||||
|
## 4. When subagents are offered
|
||||||
|
|
||||||
|
The size setting `agents {min, max}` (`team.md` §2) is shown as **Subagents** — `subagents.setting`:
|
||||||
|
the label, and **Min** and **Max** for `min` and `max` (defaults 1 and 4, at most `cap` 32, kept
|
||||||
|
with the chat). **The numbers count the session's own agent**: Min 1 / Max 1 is the agent alone,
|
||||||
|
with no subagents; Max 4 is the agent and up to three subagents (`subagents.setting.hint`). A UI
|
||||||
|
shows the setting as **one range from 1 to `cap` with two handles, Min and Max** (Min never past
|
||||||
|
Max): a slider in a graphical UI, two numbers on one line in a terminal, each moved on its own. It, the team tools and `blocks/team_lead.md` are **offered** when both hold:
|
||||||
|
|
||||||
|
1. Subagents are enabled for the session (`team.md` §2, rule 1).
|
||||||
|
2. **At least one model could serve as a subagent**: S itself, when it serves two or more requests
|
||||||
|
at once (`parallel ≥ 2`, or no limit); or **any other model the person may use that is not on
|
||||||
|
S's one-model server** (no `group`, or another one) — whatever its own `parallel`.
|
||||||
|
|
||||||
|
So a session on a one-request model is offered subagents when another server has a model, and
|
||||||
|
**its first note says so** (`context.note.team_own`, after `team`): the session's model cannot
|
||||||
|
serve subagents itself, and they need a model the person names. A UI showing the setting says the
|
||||||
|
same beside it (`subagents.setting.own`). Nothing is offered when neither holds — the same as
|
||||||
|
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: <tool's key>, 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`.
|
||||||
|
The subagents under a sub-manager that ended by itself (`done`, `out_of_context`) move under its
|
||||||
|
parent until the main agent replaces it, and then back under the replacement; a stopped one's
|
||||||
|
subtree is stopped with it (§10).
|
||||||
|
|
||||||
|
**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 <name>`), 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.
|
||||||
|
|
||||||
|
## 14. In an agent chat
|
||||||
|
|
||||||
|
The main agent of an agent chat is the CLI on the linked device: it starts and runs the
|
||||||
|
subagents there, under every rule above. The server carries the chat's Subagents setting down with
|
||||||
|
every turn and the tree, approvals, findings and transcripts up, and offers no subagents of its own
|
||||||
|
to an agent chat. The protocol: `acp.md`, *Subagents in an agent chat* (capabilities `agents` and
|
||||||
|
`tree`).
|
||||||
|
|
||||||
+263
@@ -0,0 +1,263 @@
|
|||||||
|
# Teams
|
||||||
|
|
||||||
|
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
|
||||||
|
own, so a team grows from a few agents side by side into a small company with one goal. The same in
|
||||||
|
a chat on the server and in an agent session on a device, in both projects.
|
||||||
|
|
||||||
|
The tools are `tools/team_add.json`, `team_send.json`, `team_read.json`, `team_stop.json`; the texts
|
||||||
|
and numbers `loop.json` `team` (and the size notes in `context.note`); the prompt texts
|
||||||
|
`prompts/blocks/team_lead.md` and `team_member.md`; the cases `conformance/team.json`.
|
||||||
|
|
||||||
|
> **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
|
||||||
|
it was added under. Names are unique in the team.
|
||||||
|
- **Member**: an agent added with `team_add`, by the lead or by a member that leads. Its **manager**
|
||||||
|
is the agent that added it. The members an agent added, and theirs, down the tree, are **under**
|
||||||
|
it; with the agent itself they are its **subtree**.
|
||||||
|
- **Manager**: an agent with at least one member, and the lead from the start when the team's `min`
|
||||||
|
is 2 or more (§2). A manager does not do the work (§6).
|
||||||
|
- **Team**: the lead and every member, one per lead session; **its size** is how many agents it has
|
||||||
|
now (§3).
|
||||||
|
- **Room**: the team's messages, numbered from 1 in the order they were posted, each with `n`, `from`
|
||||||
|
(a name), `to` (as addressed: names, or `all`), its recipients (resolved when it was posted), the
|
||||||
|
text, the time, and whether it is a report.
|
||||||
|
|
||||||
|
## 2. The size setting, and when a team is offered
|
||||||
|
|
||||||
|
A chat has the setting **`agents: {min, max}`** — the agents the team may have, the lead included.
|
||||||
|
**The numbers count the chat's own agent**: `1` is that agent alone, with no subagents; `max` 4 is
|
||||||
|
that agent and up to three subagents. Nothing that shows or reads the setting counts subagents only.
|
||||||
|
|
||||||
|
- `1 ≤ min ≤ max ≤ cap`. `cap` is the instance's (LLeMbas) or the global configuration's (the CLI),
|
||||||
|
default `loop.json` `team.agents.cap` (32); a project's configuration never raises it. A value
|
||||||
|
outside is refused where it is set, with what the bounds are, never clamped silently; a stored
|
||||||
|
setting the cap was lowered under is read as `max = min(max, cap)`, `min = min(min, max)`.
|
||||||
|
- Defaults for a new chat: `min` 1, `max` 4 (`team.agents`) — the lead and up to three members. LLeMbas
|
||||||
|
keeps a per-person default for new chats, seeded with those; the CLI reads them from its
|
||||||
|
configuration.
|
||||||
|
- The setting is kept with the chat. A change during a session is told to the lead as a note on its
|
||||||
|
next user message (`context.md` §10; `context.note.team`, below), never by changing the system
|
||||||
|
prompt.
|
||||||
|
|
||||||
|
**A team is offered** — the setting shown, the team tools listed, `blocks/team_lead.md` in the
|
||||||
|
system prompt — when both hold, and only then:
|
||||||
|
|
||||||
|
1. Subagents are enabled for the session: LLeMbas — the instance allows them and the chat's model
|
||||||
|
has its subagent tool; the CLI — its subagents are not switched off in the configuration.
|
||||||
|
2. **At least one model could serve as a subagent** (spec 3.3.0, `subagents.md` §4): the chat's
|
||||||
|
model, when it serves at least two requests at once (`capacity.parallel ≥ 2`, or neither
|
||||||
|
`parallel` nor `single_session` is set — a hosted API; `models/schema.json`); or any other model
|
||||||
|
the person may use that is not on the chat's model's one-model server (`capacity.group`).
|
||||||
|
|
||||||
|
The setting is shown as **Subagents**, with **Min** and **Max** (`loop.json` `subagents.setting`):
|
||||||
|
**one range from 1 to `cap` with two handles**, Min and Max, where Min never passes Max — a slider in
|
||||||
|
a graphical UI; in a terminal, the two numbers on one line, each moved on its own. Beside it, its
|
||||||
|
hint (`subagents.setting.hint`, or `short` where a line has no room): the count includes the agent,
|
||||||
|
and 1 is the agent alone. Never *fewest* / *most*, and never a count of subagents alone.
|
||||||
|
|
||||||
|
Both are fixed for as long as the system prompt is (`context.md` §10, rule 4: a model switch makes
|
||||||
|
a new prompt and a new list). Otherwise the setting is not shown and none of the four tools is
|
||||||
|
listed, and the session's `task` works as before.
|
||||||
|
|
||||||
|
While the tools are listed they need **`team`** (`requires`): met when `max ≥ 2` and a model could
|
||||||
|
still serve as a subagent (rule 2) — or, for the lead, when its team still has a member (so a lead whose person
|
||||||
|
lowered `max` to 1 can still talk to, read and stop the members it has). A call while it is not
|
||||||
|
met answers, word for word, `team.unavailable.one_request` when no model could serve as a subagent
|
||||||
|
any more (the model's metadata changed after the prompt was made), else `team.unavailable.one_agent`
|
||||||
|
— which is also the tools' own `unavailable` text.
|
||||||
|
|
||||||
|
The meaning of the setting:
|
||||||
|
|
||||||
|
- **`max = 1`**: one agent, no team, and no subagent of any kind: `task` answers `one_agent` too
|
||||||
|
(while the lead has no member; with members left from a larger setting, `full`).
|
||||||
|
- **`min ≥ 2`**: the lead works through a team from the start — it is a manager before its first
|
||||||
|
member (§6), and the note asks for at least `min − 1` members.
|
||||||
|
- **`min = 1`, `max ≥ 2`**: the lead decides; a small job it may do alone. Once it has a member it is
|
||||||
|
a manager.
|
||||||
|
|
||||||
|
## 3. Size, and what counts
|
||||||
|
|
||||||
|
**The size** of a team is: the lead, every member (whatever its state — a finished member can be
|
||||||
|
woken by a message, so it keeps its place), and every `task` subagent running in the team, started by
|
||||||
|
any agent in it (the CLI's background `task` jobs and foreground ones alike, while they run).
|
||||||
|
A stopped member does not count: stopping is how room is made.
|
||||||
|
|
||||||
|
- **`team_add` is refused past `max`** — when the size would become more than `max`, with
|
||||||
|
`team.texts.full`. So is `task`, while a team is offered: with `full`, or with `one_agent` when
|
||||||
|
`max` is 1 and the lead has no member. A `job_send` that would start a finished background
|
||||||
|
`task` subagent again is refused the same way.
|
||||||
|
- **Lowering `max` stops nobody.** Members already there stay; nothing new is added until the size
|
||||||
|
is under the new `max`.
|
||||||
|
- The size is across **the whole tree**: a member that leads adds within the same `max` as the lead.
|
||||||
|
|
||||||
|
## 4. Members
|
||||||
|
|
||||||
|
- `team_add` starts the member **at once, in the background**, under the caller: the existing job
|
||||||
|
machinery (a hidden chat on LLeMbas with its `chat_jobs` row, a subagent job in the CLI), running
|
||||||
|
unattended — no `ask_user`, the chat's own permission ceiling, its mode no looser than its
|
||||||
|
manager's (`permission/modes.json`).
|
||||||
|
- **Its agent** is `general` (the default: it does the work) or `explore` (read-only). **Its model**
|
||||||
|
is the session's (the lead's) unless `model` gives one the person named in this session — as
|
||||||
|
`task`'s, refused with `subagents.texts` otherwise (`subagents.md` §2, §3).
|
||||||
|
- **Its tools** are those its agent gives a subagent, plus `team_send` and `team_read`; one added
|
||||||
|
with `lead: true` also has `team_add` and `team_stop`. They are fixed when it is made (§10 rule 4:
|
||||||
|
a subagent's narrowing). `report` (`offered: subagent`) sends its manager a message before its
|
||||||
|
final reply: posted in the room to the manager, delivered with the message mark.
|
||||||
|
- **Its system prompt** is built once, when it is added: what a subagent of that agent gets, with
|
||||||
|
`blocks/team_member.md` in place of `blocks/subagent.md` — its name, role, manager, the team's
|
||||||
|
goal, the agent's own instructions, and for one with `lead: true` the whole of
|
||||||
|
`blocks/team_lead.md`. Its first user message is its brief (`task`), as given.
|
||||||
|
- **The team's goal** is what a member is told the team is for: the `goal` the caller gave in
|
||||||
|
`team_add`, else the caller's own goal. The lead's own goal is the last `goal` it gave; **the
|
||||||
|
lead's first `team_add` must give one** (`team.texts.no_goal`). A member already working keeps the
|
||||||
|
goal it was told; a new goal reaches it only as a message.
|
||||||
|
- **`team_add`'s refusals are checked in this order**, the first that applies answering:
|
||||||
|
`unavailable` (§2); `all_name` (the name `all`); `bad_name` (not `team.names.pattern`); `taken`
|
||||||
|
(the name of an agent on the team, `lead` included); `no_goal`; `full`. Then the member is added:
|
||||||
|
`added`, or `added_waiting` when it has to wait for a slot (§8). An unknown `agent` or `model`
|
||||||
|
answers as `task` does; then `model`'s refusals, `not_named`, `same_group`, `one_request`
|
||||||
|
(`subagents.md`), in that order — before anything is added.
|
||||||
|
- **Members are not jobs to the model.** `job_list` does not show them and the `job_*` tools do not
|
||||||
|
take them; the team tools do. (An implementation may run them on its job machinery.)
|
||||||
|
- **States**: `working` (a turn is running), `waiting` (it has work, and waits for a slot, §8),
|
||||||
|
`finished` (its last turn ended; it can be woken). The lead is `working`, `waiting` or `idle`.
|
||||||
|
- A member's background commands stop when its turn ends (`loop.json` `jobs.lifecycle`). Deleting
|
||||||
|
the lead's session stops the whole team.
|
||||||
|
|
||||||
|
## 5. The room: who receives what
|
||||||
|
|
||||||
|
- **`team_send`** posts one message: `to` a list of names, or `["all"]` (a lone string is read as a
|
||||||
|
one-name list). Names are deduplicated, kept in the order given. `all` anywhere in the list means
|
||||||
|
all, and the other names in it are ignored.
|
||||||
|
- **Recipients**: for named agents, exactly those named; for `all`, every agent of the team but the
|
||||||
|
sender — the lead and every member, whoever manages them (the room is one channel for the whole
|
||||||
|
company). Resolved when the message is posted: an agent added later does not get it.
|
||||||
|
- **Refused, and nothing posted**, checked in this order: `all` when nobody else is on the team
|
||||||
|
(`nobody`); a name that is not on the team (`unknown`, the first such name in the order given);
|
||||||
|
the sender's own name among them (`self`). The answer when
|
||||||
|
posted: `team.texts.sent`, `to` as addressed.
|
||||||
|
- **What an agent receives** is only what is addressed to it, by name or as `all`, and — a manager
|
||||||
|
— **the reports of its own members**. A manager is not sent its members' messages to each other;
|
||||||
|
it reads them with `team_read`, on demand, so nothing floods its context.
|
||||||
|
- **A report**: a member's final reply, when its turn ends, is posted to its manager (`report`
|
||||||
|
true) and delivered to it. An empty reply reads `team.texts.no_report`; a turn that ended
|
||||||
|
otherwise than by the model stopping adds `team.texts.ended` (`steps`, `budget`, `error — <text>`),
|
||||||
|
after a blank line. A sub-manager's final reply is likewise its report to its manager.
|
||||||
|
- **The person's** messages go to the lead as its user messages, not into the room; the lead's
|
||||||
|
answers to the person are not room messages either. The person sees the whole room and the tree
|
||||||
|
in the UI (LLeMbas: the Team panel; the CLI: `/team`), and can stop a member there — said to the
|
||||||
|
lead as a note (`context.note.team_stopped`).
|
||||||
|
- **`team_read`** answers the tree — every agent, the whole team, in tree order: name, role, its
|
||||||
|
manager, state, model, last activity — and the room's messages numbered above `since` (default 0)
|
||||||
|
that the caller may see, oldest first, at most `team.read_max` (50), and how to read on. **Who may
|
||||||
|
see a message**: the lead every one; any other agent those whose sender or one of whose recipients
|
||||||
|
is in its subtree — for a member that leads nobody, what it sent and what it received.
|
||||||
|
|
||||||
|
## 6. A manager does not do the work
|
||||||
|
|
||||||
|
A manager's **work** calls are refused, before the permission check — nothing is asked — with
|
||||||
|
`team.texts.manager`, word for word:
|
||||||
|
|
||||||
|
> You lead a team: give this to a member (team_add or team_send).
|
||||||
|
|
||||||
|
**Work** is a call whose risk class (`risk`; an MCP tool's: `read` when it says `readOnlyHint`, else
|
||||||
|
`execute`) is `write` or `execute`, with two exceptions that stay a manager's:
|
||||||
|
|
||||||
|
- `job_stop` — managing what runs is not work;
|
||||||
|
- a `bash` call **the default rules alone allow in manual mode** (`permission/defaults.json`, the
|
||||||
|
`permission` conformance area's evaluation with no rules of the person's): reading commands —
|
||||||
|
`git status`, `git diff`, `cat`, `ls`, `grep` — for review.
|
||||||
|
|
||||||
|
So with the spec's tools: **refused** — `write`, `edit`, `multiedit`, `apply_patch`, `mkdir`, `mv`,
|
||||||
|
`rm`, `skill_manage` (write); `bash` beyond reading, `python`, `commit`, `branch`, `pr`, `merge`,
|
||||||
|
`tag`, `release` (execute); an MCP tool that does not say it only reads. **Kept** — every `read`
|
||||||
|
tool (`read`, `glob`, `grep`, `list`, `search`, `recall`, `web_search`, `web_fetch`, …), every
|
||||||
|
`interact` tool (`ask_user` for the lead, `plan_*`, `task`, `memory`, `show`, `job_send`, `report`,
|
||||||
|
the team tools, …) and `job_stop`. `team.work` lists the same, and a new tool is placed by its risk
|
||||||
|
class.
|
||||||
|
|
||||||
|
**The tool list does not change** when an agent becomes a manager (`context.md` §10, rule 4): the
|
||||||
|
same tools are listed and the refused ones answer the refusal. An agent is a manager while it has
|
||||||
|
a member — a finished one included; one whose members were all stopped is not, unless it is the
|
||||||
|
lead and `min ≥ 2` with the team offered. `task` stays: a one-off subagent is delegation, and counts
|
||||||
|
toward the size (§3).
|
||||||
|
|
||||||
|
## 7. Delivery, and waking
|
||||||
|
|
||||||
|
A message or a report reaches its recipient **as a marked message** — the jobs mechanism
|
||||||
|
(`loop.json` `jobs`): while the recipient's turn runs, as a user message at its next step boundary
|
||||||
|
(which does not start a turn: `context.md` §8); otherwise it **wakes** the recipient — a finished
|
||||||
|
member, or the idle lead — as a new turn of its own, marked `machine`, with its context intact, as
|
||||||
|
`job_send` restarts a finished subagent. A woken agent needs a slot (§8). Several waiting at one step
|
||||||
|
boundary go in one user message, in room order, separated by a blank line.
|
||||||
|
|
||||||
|
Every one begins with its mark, on a line of its own, then the text:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Team message from parser to lead:]
|
||||||
|
The grammar is done; the tests are next.
|
||||||
|
```
|
||||||
|
|
||||||
|
- message: `[Team message from {{from}} to {{to}}:]` — `to` as addressed: the names joined with
|
||||||
|
`, `, or `all`; the same for every recipient.
|
||||||
|
- report: `[Team report from {{from}} to {{to}}:]` — `to` the manager.
|
||||||
|
|
||||||
|
A session that is not open keeps them until it is resumed, as a job's note.
|
||||||
|
|
||||||
|
## 8. Capacity: a member waits for a slot
|
||||||
|
|
||||||
|
A model serves `parallel` requests at once (`models/schema.json`). **An agent holds a slot of its
|
||||||
|
model while its turn runs** — the lead, a member, a `task` subagent. An agent with work (added, woken,
|
||||||
|
given a message while finished, the lead given the person's message) starts when its model has a
|
||||||
|
free slot and none of its `capacity.group`'s other models is busy; otherwise it is `waiting`.
|
||||||
|
`team_add` is never refused for a full model (only for size, and for the model rules of
|
||||||
|
`subagents.md` §3: another model on the lead's one-model server, or a one-request session model):
|
||||||
|
its answer says the member waits.
|
||||||
|
|
||||||
|
Waiting agents start **in order**: the lead first (the person waits on it), then the others in the
|
||||||
|
order they began waiting. A slot frees when a turn ends or an agent is stopped. `task` keeps its own
|
||||||
|
rule (`loop.json` `capacity`: refused where its model is full), and its subagent holds a slot.
|
||||||
|
|
||||||
|
## 9. Stopping
|
||||||
|
|
||||||
|
`team_stop` stops one member **and its whole subtree**, or with `all` every member under the caller:
|
||||||
|
their turns stop where they are, their background commands with them, messages waiting for them
|
||||||
|
are dropped, and they **leave the team** — the size drops, their slots free, their names may be
|
||||||
|
used again. No report comes back from a stopped member. Refused, in this order: `all` with nobody
|
||||||
|
under the caller (`nobody_to_stop`); a name not on the team (`unknown`); the caller itself or an
|
||||||
|
agent not under it (`not_yours`). The answer, `stopped`, names the stopped in tree order.
|
||||||
|
|
||||||
|
## 10. What the lead is told
|
||||||
|
|
||||||
|
- `blocks/team_lead.md` — constant, no numbers — is in the system prompt whenever the team tools
|
||||||
|
are listed: how to lead.
|
||||||
|
- The size is in a **note** (`context.md` §10, `context.note`): on the first user message of the
|
||||||
|
session, and on the next one after the setting changed — `team` (`min`, `max`), with `team_lead`
|
||||||
|
after it when `min ≥ 2` (`members` = `min − 1`); when `max` is 1, `team_one` instead. One block.
|
||||||
|
A member the person stopped is its own block, `team_stopped`, with the names. Members are told
|
||||||
|
nothing of the size: `team_add`'s answer says how big the team is.
|
||||||
|
|
||||||
|
## 11. Over the link
|
||||||
|
|
||||||
|
*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 `<name>: <role>`), so the client's job strip shows them, and the room is shown
|
||||||
|
on the device (`/team`). Carrying the room is a later spec's.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "apply_patch",
|
"name": "apply_patch",
|
||||||
"scope": "cli",
|
"scope": "cli",
|
||||||
|
"sessions": "agent",
|
||||||
"risk": "write",
|
"risk": "write",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "edit",
|
"block": "edit",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ask_user",
|
"name": "ask_user",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "interact",
|
"risk": "interact",
|
||||||
"exclusive": true,
|
"exclusive": true,
|
||||||
"block": "tool",
|
"block": "tool",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bash",
|
"name": "bash",
|
||||||
"scope": "cli",
|
"scope": "cli",
|
||||||
|
"sessions": "agent",
|
||||||
"risk": "execute",
|
"risk": "execute",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "shell",
|
"block": "shell",
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "branch",
|
||||||
|
"scope": "llembas",
|
||||||
|
"sessions": "chat",
|
||||||
|
"risk": "execute",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": "purpose",
|
||||||
|
"requires": [
|
||||||
|
"forge"
|
||||||
|
],
|
||||||
|
"unavailable": "No repository from a forge is open to this session, so there is nothing to branch here.",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Create a branch in a repository (a mount), from `from` (default: the branch this session works on), and switch this session's working copy of it to the new branch. Staged changes come along.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Any path inside the repository (the mount) this is about"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The new branch's name"
|
||||||
|
},
|
||||||
|
"from": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Branch, tag or commit to start from"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"path",
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "commit",
|
||||||
|
"scope": "llembas",
|
||||||
|
"sessions": "chat",
|
||||||
|
"risk": "execute",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": "purpose",
|
||||||
|
"requires": [
|
||||||
|
"forge"
|
||||||
|
],
|
||||||
|
"unavailable": "No repository from a forge is open to this session, so there is nothing to commit here.",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Send the staged changes of a repository (a mount) to the forge as one commit on the branch this session works on, under the user's account. Every change this session made in that repository since its last commit goes in, unless `paths` names some of them.\n\n- Refused when the branch has moved on the forge since this session's working copy was based on it: the answer shows what changed there. Look at it, then commit again.\n- Write the message as the repository's history does: a short subject line, then why, where it is not obvious.\n- Needs commit access to the repository in this session. With stage access the changes stay staged, for the user to commit.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Any path inside the repository (the mount) this is about"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The commit message"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Only these changed paths (default: every staged change)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"path",
|
||||||
|
"message"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "decisions",
|
"name": "decisions",
|
||||||
"scope": "cli",
|
"scope": "cli",
|
||||||
|
"sessions": "agent",
|
||||||
"risk": "interact",
|
"risk": "interact",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "tool",
|
"block": "tool",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "edit",
|
"name": "edit",
|
||||||
"scope": "cli",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "write",
|
"risk": "write",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "edit",
|
"block": "edit",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "glob",
|
"name": "glob",
|
||||||
"scope": "cli",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "search",
|
"block": "search",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "grep",
|
"name": "grep",
|
||||||
"scope": "cli",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "search",
|
"block": "search",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "job_list",
|
"name": "job_list",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "tool",
|
"block": "tool",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "job_output",
|
"name": "job_output",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "tool",
|
"block": "tool",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "job_send",
|
"name": "job_send",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "interact",
|
"risk": "interact",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "tool",
|
"block": "tool",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "job_stop",
|
"name": "job_stop",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "execute",
|
"risk": "execute",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "tool",
|
"block": "tool",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "knowledge_get",
|
"name": "knowledge_get",
|
||||||
"scope": "shared",
|
"scope": "cli",
|
||||||
|
"sessions": "agent",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "read",
|
"block": "read",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "knowledge_search",
|
"name": "knowledge_search",
|
||||||
"scope": "shared",
|
"scope": "cli",
|
||||||
|
"sessions": "agent",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "search",
|
"block": "search",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "list",
|
"name": "list",
|
||||||
"scope": "cli",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "search",
|
"block": "search",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "memory",
|
"name": "memory",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "interact",
|
"risk": "interact",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "tool",
|
"block": "tool",
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "merge",
|
||||||
|
"scope": "llembas",
|
||||||
|
"sessions": "chat",
|
||||||
|
"risk": "execute",
|
||||||
|
"exclusive": true,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": "purpose",
|
||||||
|
"requires": [
|
||||||
|
"forge"
|
||||||
|
],
|
||||||
|
"confirm": "always",
|
||||||
|
"unavailable": "No repository from a forge is open to this session, so there is nothing to merge here.",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Merge a pull request on the forge. The user is always asked first, in every mode, and shown the pull request: nothing is merged without their yes.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Any path inside the repository (the mount) this is about"
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "The pull request's number"
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"merge",
|
||||||
|
"squash",
|
||||||
|
"rebase"
|
||||||
|
],
|
||||||
|
"description": "How to merge (default: the repository's setting)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"path",
|
||||||
|
"number"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "mkdir",
|
||||||
|
"scope": "llembas",
|
||||||
|
"sessions": "chat",
|
||||||
|
"risk": "write",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": "purpose",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Make a directory, and any missing directories above it. Making one that already exists is not an error.\n\n- `path` may be absolute or relative to the working directory.\n- In a repository (a mount), an empty directory stays in this session's working copy; a commit leaves empty directories out.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Directory to make"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"path"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "multiedit",
|
"name": "multiedit",
|
||||||
"scope": "cli",
|
"scope": "cli",
|
||||||
|
"sessions": "agent",
|
||||||
"risk": "write",
|
"risk": "write",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "edit",
|
"block": "edit",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "mv",
|
||||||
|
"scope": "llembas",
|
||||||
|
"sessions": "chat",
|
||||||
|
"risk": "write",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": "purpose",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Move or rename a file or directory. A directory moves with everything in it.\n\n- `from` and `to` may be absolute or relative to the working directory; `to` must not exist yet.\n- Inside one repository (a mount) the move is staged like any other change. Between a repository and the rest of the file system, or between two repositories, it is refused: read the file and write it where it should go instead.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"from": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "What to move"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The new path, including the name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"from",
|
||||||
|
"to"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "note_manage",
|
"name": "note_manage",
|
||||||
"scope": "shared",
|
"scope": "cli",
|
||||||
|
"sessions": "agent",
|
||||||
"risk": "interact",
|
"risk": "interact",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "tool",
|
"block": "tool",
|
||||||
@@ -53,12 +54,5 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"action"
|
"action"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"narrow": {
|
|
||||||
"llembas": {
|
|
||||||
"drop": [
|
|
||||||
"scope"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "note_view",
|
"name": "note_view",
|
||||||
"scope": "shared",
|
"scope": "cli",
|
||||||
|
"sessions": "agent",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "read",
|
"block": "read",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "notes_search",
|
"name": "notes_search",
|
||||||
"scope": "shared",
|
"scope": "cli",
|
||||||
|
"sessions": "agent",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "search",
|
"block": "search",
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
{
|
||||||
|
"name": "phone_action",
|
||||||
|
"scope": "llembas",
|
||||||
|
"sessions": "chat",
|
||||||
|
"risk": "interact",
|
||||||
|
"exclusive": true,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": false,
|
||||||
|
"requires": [
|
||||||
|
"phone"
|
||||||
|
],
|
||||||
|
"unavailable": "This needs the LLeMbas app on the person's phone, and this message did not come from it: nothing was done on any phone. Say what you would have done, and that they can ask again from the app.",
|
||||||
|
"confirm": "always",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Do something on the person's phone: they are writing to you from the LLeMbas app on it, and the app carries it out with the phone's own apps. The person confirms every call on the phone before anything happens; the result says done, declined or failed, with what the phone said.\n\nUse it only for what the person asked for, one action per call. `summary` is the one line the phone shows them: say what will happen, in their language.\n\n- alarm: `time` (HH:MM, 24-hour); `days` to repeat it; `label`.\n- timer: `seconds`; `label`.\n- calendar_event: `title`, `start` (YYYY-MM-DDTHH:MM, the phone's local time; YYYY-MM-DD with all_day); `end`, `all_day`, `location`, `notes`.\n- sms_draft: `to` (numbers, or names as the person said them), `body`. A draft: the person sends it.\n- email_draft: `to`, `cc`, `subject`, `body`. A draft: the person sends it.\n- dial: `number`. Opens the dialer with it; the person places the call.\n- navigate: `destination`; `stops` on the way, in order; `mode` (drive, walk, bike, transit); `replace_route` to start over rather than ask.\n- add_stop: `stop`, and `destination` — the route's destination as the person told you. Keep the destination of a route you started: no map app can add to a running route, so the phone plans again to the stop and then the destination.\n- search_nearby: `query` (\"petrol station\"), `near` me or the route.\n- stop_navigation: ends the route. Not every map app can; the phone says so if not.\n- open_app: `app`, as the person names it.\n- open_url: `url` (http or https).\n- media: `command` (play, pause, toggle, next, previous) for what is playing.\n- torch: `on`.\n- settings_panel: `panel` — the phone's quick panel or page for it; the person flips the switch.\n- automation_intent: `task`, the name of an automation the person made (Tasker by default, or `app`); `values` it is given.\n\nA place is lat and lon when you know them, else a `query` the map app searches; `name` is what the person calls it.\n\nDeclined means they said no on the phone: do not try again unless they ask. Failed says why; nothing was done.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"alarm",
|
||||||
|
"timer",
|
||||||
|
"calendar_event",
|
||||||
|
"sms_draft",
|
||||||
|
"email_draft",
|
||||||
|
"dial",
|
||||||
|
"navigate",
|
||||||
|
"add_stop",
|
||||||
|
"search_nearby",
|
||||||
|
"stop_navigation",
|
||||||
|
"open_app",
|
||||||
|
"open_url",
|
||||||
|
"media",
|
||||||
|
"torch",
|
||||||
|
"settings_panel",
|
||||||
|
"automation_intent"
|
||||||
|
],
|
||||||
|
"description": "What to do"
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "One line the phone shows the person: what will happen"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "alarm: HH:MM, 24-hour"
|
||||||
|
},
|
||||||
|
"days": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"mon",
|
||||||
|
"tue",
|
||||||
|
"wed",
|
||||||
|
"thu",
|
||||||
|
"fri",
|
||||||
|
"sat",
|
||||||
|
"sun"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "alarm: the days it repeats on; none for once"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "alarm, timer: its name on the phone"
|
||||||
|
},
|
||||||
|
"seconds": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 86400,
|
||||||
|
"description": "timer: how long"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "calendar_event: the event's title"
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "calendar_event: YYYY-MM-DDTHH:MM, local; YYYY-MM-DD with all_day"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "calendar_event: as start"
|
||||||
|
},
|
||||||
|
"all_day": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "calendar_event: a whole day"
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "calendar_event: where"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "calendar_event: its description"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "sms_draft: numbers or names; email_draft: addresses"
|
||||||
|
},
|
||||||
|
"cc": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "email_draft: addresses"
|
||||||
|
},
|
||||||
|
"subject": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "email_draft"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "sms_draft, email_draft: the text"
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "dial: the phone number"
|
||||||
|
},
|
||||||
|
"destination": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "navigate, add_stop: where the route ends",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "What the person calls it: \"Home\", \"Lidl on Main Street\""
|
||||||
|
},
|
||||||
|
"lat": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -90,
|
||||||
|
"maximum": 90,
|
||||||
|
"description": "Latitude, when known"
|
||||||
|
},
|
||||||
|
"lon": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -180,
|
||||||
|
"maximum": 180,
|
||||||
|
"description": "Longitude, when known"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "What the map app searches when there are no coordinates"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stops": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "A place: lat and lon when you know them, else a query the map app searches (an address, or a name and a town); name is what the person calls it",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "What the person calls it: \"Home\", \"Lidl on Main Street\""
|
||||||
|
},
|
||||||
|
"lat": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -90,
|
||||||
|
"maximum": 90,
|
||||||
|
"description": "Latitude, when known"
|
||||||
|
},
|
||||||
|
"lon": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -180,
|
||||||
|
"maximum": 180,
|
||||||
|
"description": "Longitude, when known"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "What the map app searches when there are no coordinates"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "navigate: places on the way, in order"
|
||||||
|
},
|
||||||
|
"stop": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "add_stop: the place to add",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "What the person calls it: \"Home\", \"Lidl on Main Street\""
|
||||||
|
},
|
||||||
|
"lat": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -90,
|
||||||
|
"maximum": 90,
|
||||||
|
"description": "Latitude, when known"
|
||||||
|
},
|
||||||
|
"lon": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -180,
|
||||||
|
"maximum": 180,
|
||||||
|
"description": "Longitude, when known"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "What the map app searches when there are no coordinates"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"drive",
|
||||||
|
"walk",
|
||||||
|
"bike",
|
||||||
|
"transit"
|
||||||
|
],
|
||||||
|
"description": "navigate: how the person travels (drive when not said)"
|
||||||
|
},
|
||||||
|
"replace_route": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "navigate: replace a route already running without asking"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "search_nearby: what to look for"
|
||||||
|
},
|
||||||
|
"near": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"me",
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"description": "search_nearby: around the person, or along the running route"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "open_app: the app, as the person names it; automation_intent: the automation app (Tasker when not said)"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "open_url: an http or https address"
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"play",
|
||||||
|
"pause",
|
||||||
|
"toggle",
|
||||||
|
"next",
|
||||||
|
"previous"
|
||||||
|
],
|
||||||
|
"description": "media"
|
||||||
|
},
|
||||||
|
"on": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "torch: on or off"
|
||||||
|
},
|
||||||
|
"panel": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"wifi",
|
||||||
|
"bluetooth",
|
||||||
|
"internet",
|
||||||
|
"volume",
|
||||||
|
"nfc",
|
||||||
|
"location",
|
||||||
|
"do_not_disturb"
|
||||||
|
],
|
||||||
|
"description": "settings_panel"
|
||||||
|
},
|
||||||
|
"task": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "automation_intent: the automation's name"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "automation_intent: named values it is given"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"action",
|
||||||
|
"summary"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "plan_submit",
|
"name": "plan_submit",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "interact",
|
"risk": "interact",
|
||||||
"exclusive": true,
|
"exclusive": true,
|
||||||
"block": "tool",
|
"block": "tool",
|
||||||
"purpose": false,
|
"purpose": false,
|
||||||
|
"requires": [
|
||||||
|
"plan"
|
||||||
|
],
|
||||||
|
"unavailable": "Not in plan mode: plan_submit is how a plan made in plan mode is put to the person for approval. Carry on with the work; keep the plan current with plan_update.",
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"cli": [
|
"cli": [
|
||||||
"plan_exit"
|
"plan_exit"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "plan_update",
|
"name": "plan_update",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "interact",
|
"risk": "interact",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "plan",
|
"block": "plan",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "plan_write",
|
"name": "plan_write",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "interact",
|
"risk": "interact",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "plan",
|
"block": "plan",
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "pr",
|
||||||
|
"scope": "llembas",
|
||||||
|
"sessions": "chat",
|
||||||
|
"risk": "execute",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": "purpose",
|
||||||
|
"requires": [
|
||||||
|
"forge"
|
||||||
|
],
|
||||||
|
"unavailable": "No repository from a forge is open to this session, so there is nothing to pr here.",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Open a pull request on the forge, from `head` (default: the branch this session works on) into `base` (default: the repository's default branch). Only committed changes are in it: commit first.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Any path inside the repository (the mount) this is about"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "What it changes and why"
|
||||||
|
},
|
||||||
|
"head": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The branch with the changes"
|
||||||
|
},
|
||||||
|
"base": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The branch to merge into"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"path",
|
||||||
|
"title"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "python",
|
||||||
|
"scope": "llembas",
|
||||||
|
"sessions": "chat",
|
||||||
|
"risk": "execute",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "shell",
|
||||||
|
"purpose": "purpose",
|
||||||
|
"requires": [
|
||||||
|
"page"
|
||||||
|
],
|
||||||
|
"unavailable": "Python cannot run now: no open browser page is attached to this chat.",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"variables": {
|
||||||
|
"packages": "the packages the instance provides beyond the standard library, as a list, or \"none\" — implementation-defined; LLeMbas 2.0: none (the standard library only)",
|
||||||
|
"seconds": "the time limit of one call, in seconds — implementation-defined; LLeMbas 2.0: 30",
|
||||||
|
"memory": "the memory limit of one call, with its unit — implementation-defined; LLeMbas 2.0: 512 MB",
|
||||||
|
"output": "how much printed output comes back, with its unit — implementation-defined; LLeMbas 2.0: 20,000 characters"
|
||||||
|
},
|
||||||
|
"description": "Run Python for calculations and for working with files — parse JSON or CSV, transform text, compute. It runs in the user's browser (Pyodide), sandboxed: no network, no shell, no packages beyond the standard library and these: {{packages}}. Returns what the code printed and the value of its last expression.\n\n- `files` are copied in before the run, at the same paths, from what this session can read.\n- Files the code writes come back only when listed in `save`: each is written as the write tool would write it, with this session's permissions.\n- At most {{seconds}} seconds and {{memory}} of memory per call; printed output past {{output}} is cut.\n- Every call starts fresh: nothing is kept between calls.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The Python code to run"
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Files to copy in before the run"
|
||||||
|
},
|
||||||
|
"save": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Files the code writes that are to be saved back"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "read",
|
"name": "read",
|
||||||
"scope": "cli",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "read",
|
"block": "read",
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "recall",
|
||||||
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
|
"risk": "read",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "read",
|
||||||
|
"purpose": false,
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Show a stored tool result again, in full, by its id (`#a3f2`): the lines a shortened output left out, or a result from before the conversation was summarised. Every result of this session is kept, so nothing is lost.\n\n- Returns up to 2000 lines from `offset` (1-indexed); call again with a larger offset for more.\n- An unchanged file is not worth recalling when its earlier result is still above: that result is the content.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The result's id, e.g. #a3f2"
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "First line to return, 1-indexed"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "How many lines (default 2000)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "release",
|
||||||
|
"scope": "llembas",
|
||||||
|
"sessions": "chat",
|
||||||
|
"risk": "execute",
|
||||||
|
"exclusive": true,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": "purpose",
|
||||||
|
"requires": [
|
||||||
|
"forge"
|
||||||
|
],
|
||||||
|
"confirm": "always",
|
||||||
|
"unavailable": "No repository from a forge is open to this session, so there is nothing to release here.",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Publish a release on the forge from a tag that already exists, with its notes. Make the tag first (tag): a release is never made for a tag that does not exist. The user is always asked first, in every mode.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Any path inside the repository (the mount) this is about"
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The existing tag"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The release notes (usually the tag's message)"
|
||||||
|
},
|
||||||
|
"prerelease": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"draft": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"path",
|
||||||
|
"tag",
|
||||||
|
"title",
|
||||||
|
"notes"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "report",
|
"name": "report",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "interact",
|
"risk": "interact",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "tool",
|
"block": "tool",
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "rm",
|
||||||
|
"scope": "llembas",
|
||||||
|
"sessions": "chat",
|
||||||
|
"risk": "write",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": "purpose",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Delete a file or directory by moving it to the trash, from which the user can restore it. A directory goes with everything in it: list it first.\n\n- `path` may be absolute or relative to the working directory.\n- In a repository (a mount) the deletion is staged in this session's working copy and reaches the repository only with a commit.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "File or directory to delete"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"path"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "search",
|
||||||
|
"scope": "llembas",
|
||||||
|
"sessions": "chat",
|
||||||
|
"risk": "read",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "search",
|
||||||
|
"purpose": "purpose",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Search the files this session can see, by meaning and by words together: the best passages first, each with its path and line range. Use it to find where something is discussed or answered when you do not know the exact words; use grep for exact text or a pattern, glob for file names.\n\n- `path` limits the search to a directory (default: everything this session can see).\n- A passage is an extract. Read the file around it before answering from it when the answer turns on detail, and cite the path.\n- Only indexed files are searched: text, Markdown, code, JSON and the text of PDF and DOCX files, up to a size limit. A file just written is found once it has been indexed.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "What to look for, in words: a question or the topic"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Directory to search (default: everything this session can see)"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 50,
|
||||||
|
"description": "How many passages (default 10)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"query"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "session_search",
|
"name": "session_search",
|
||||||
"scope": "cli",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "search",
|
"block": "search",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "settings",
|
"name": "settings",
|
||||||
"scope": "cli",
|
"scope": "cli",
|
||||||
|
"sessions": "agent",
|
||||||
"risk": "interact",
|
"risk": "interact",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "tool",
|
"block": "tool",
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "show",
|
||||||
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
|
"risk": "interact",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": "purpose",
|
||||||
|
"requires": [
|
||||||
|
"display"
|
||||||
|
],
|
||||||
|
"unavailable": "Nothing can be shown now: this session has no screen to show a file on. Give the user the path instead.",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Open a file in the user's viewer, beside the conversation, without them having to look for it: Markdown rendered, code highlighted, plain text as it is, and HTML as a page in a sandboxed frame that cannot reach this app, the user's data or anything the instance does not allow. Use it when the user should see the file itself — a page or a chart you made, a document you wrote — not for files you only read.\n\n- `path` may be absolute or relative to the working directory. The file must exist and be readable in this session: write it first.\n- `line` scrolls a text file to that line.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "File to show"
|
||||||
|
},
|
||||||
|
"line": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "Line to scroll to (text files)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"path"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "skill_manage",
|
"name": "skill_manage",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "write",
|
"risk": "write",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "tool",
|
"block": "tool",
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The skill (lowercase letters, digits, - _ .; up to 64)"
|
"description": "The skill (lowercase letters, digits and -, starting with a letter or digit; up to 64)"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"description": "create: when to use the skill, one sentence, the trigger first: \"Use when …\"",
|
"description": "create: when to use the skill, one sentence, the trigger first: \"Use when …\"",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "skill_view",
|
"name": "skill_view",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "read",
|
"block": "read",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "skills_list",
|
"name": "skills_list",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "search",
|
"block": "search",
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "tag",
|
||||||
|
"scope": "llembas",
|
||||||
|
"sessions": "chat",
|
||||||
|
"risk": "execute",
|
||||||
|
"exclusive": true,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": "purpose",
|
||||||
|
"requires": [
|
||||||
|
"forge"
|
||||||
|
],
|
||||||
|
"confirm": "always",
|
||||||
|
"unavailable": "No repository from a forge is open to this session, so there is nothing to tag here.",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Create an annotated tag on the forge, at `at` (default: the newest commit of the branch this session works on), its message being the release note. The user is always asked first, in every mode.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Any path inside the repository (the mount) this is about"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The tag, e.g. v1.2.0"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The tag's message"
|
||||||
|
},
|
||||||
|
"at": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Branch or commit to tag"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"path",
|
||||||
|
"name",
|
||||||
|
"message"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "task",
|
"name": "task",
|
||||||
"scope": "shared",
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
"risk": "interact",
|
"risk": "interact",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "task",
|
"block": "task",
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
"subagent_run"
|
"subagent_run"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"description": "Hand a self-contained piece of work to a subagent, which works on its own and returns only its final report — keeping the searching and reading out of your context.\n\n- Use it for broad research (\"where is X handled, and what calls it?\") or a separable job. For one file you already know, just read it.\n- `prompt` must stand on its own: the subagent sees nothing of this conversation. Say what to find or do, and what to report back.\n- `background: true` returns at once with a job id and you carry on; the report comes to you as a message when it ends. Meanwhile job_output shows its progress, job_send gives it a message, job_stop stops it. Without it, the call waits for the report.\n- Independent tasks can run in the same turn. Subagents that change files at the same time should each have `worktree: true`, so they work on separate branches.\n- Agents: {{agents}}",
|
"description": "Hand a self-contained piece of work to a subagent, which works on its own and returns only its final report — keeping the searching and reading out of your context.\n\n- Use it for broad research (\"where is X handled, and what calls it?\") or a separable job. For one file you already know, just read it.\n- `prompt` must stand on its own: the subagent sees nothing of this conversation. Say what to find or do, and what to report back.\n- `background: true` returns at once with a job id and you carry on; the report comes to you as a message when it ends. Meanwhile job_output shows its progress, job_send gives it a message, job_stop stops it. Without it, the call waits for the report.\n- Independent tasks can run in the same turn. Subagents that change files at the same time should each have `worktree: true`, so they work on separate branches.\n- It runs on your model. Another model only when the person named it in this conversation; nothing else chooses one.\n- Agents: {{agents}}",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -37,7 +38,7 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"model": {
|
"model": {
|
||||||
"description": "Run it on another configured model (connection/model, from the list in your instructions) — a second opinion, or a model better at this kind of work. Default: the agent's own model, else yours.",
|
"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: your model.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "tasks",
|
"name": "tasks",
|
||||||
"scope": "cli",
|
"scope": "cli",
|
||||||
|
"sessions": "agent",
|
||||||
"risk": "interact",
|
"risk": "interact",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "tool",
|
"block": "tool",
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"name": "team_add",
|
||||||
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
|
"risk": "interact",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "task",
|
||||||
|
"purpose": false,
|
||||||
|
"requires": [
|
||||||
|
"team"
|
||||||
|
],
|
||||||
|
"unavailable": "The person allows one agent in this chat: there is no team. Do the work yourself.",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Add a 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": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z][a-z0-9-]{0,31}$",
|
||||||
|
"description": "Its name in the team, after what it owns"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Its position, one line: what it owns"
|
||||||
|
},
|
||||||
|
"task": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Its brief, self-contained"
|
||||||
|
},
|
||||||
|
"goal": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The team's goal in one line: needed with your first member; a new one is told to members added from now on"
|
||||||
|
},
|
||||||
|
"lead": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "A sub-manager: directs the subagents placed under it (default: false)"
|
||||||
|
},
|
||||||
|
"agent": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Which agent it is (default: general)"
|
||||||
|
},
|
||||||
|
"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": [
|
||||||
|
"name",
|
||||||
|
"role",
|
||||||
|
"task"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"variables": {
|
||||||
|
"agents": "the agents a member may be: `name — description`, separated by \"; \" (as task's)"
|
||||||
|
},
|
||||||
|
"offered": "main"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "team_read",
|
||||||
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
|
"risk": "read",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": false,
|
||||||
|
"requires": [
|
||||||
|
"team"
|
||||||
|
],
|
||||||
|
"unavailable": "The person allows one agent in this chat: there is no team. Do the work yourself.",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "The 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": {
|
||||||
|
"since": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Show messages after this number (default: 0, from the start)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "team_send",
|
||||||
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
|
"risk": "interact",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": false,
|
||||||
|
"requires": [
|
||||||
|
"team"
|
||||||
|
],
|
||||||
|
"unavailable": "The person allows one agent in this chat: there is no team. Do the work yourself.",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Send a message through 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": {
|
||||||
|
"to": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"description": "Names, [\"parent\"], or [\"all\"]"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"to",
|
||||||
|
"message"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "team_stop",
|
||||||
|
"scope": "shared",
|
||||||
|
"sessions": "both",
|
||||||
|
"risk": "interact",
|
||||||
|
"exclusive": false,
|
||||||
|
"block": "tool",
|
||||||
|
"purpose": false,
|
||||||
|
"requires": [
|
||||||
|
"team"
|
||||||
|
],
|
||||||
|
"unavailable": "The person allows one agent in this chat: there is no team. Do the work yourself.",
|
||||||
|
"aliases": {
|
||||||
|
"cli": [],
|
||||||
|
"llembas": []
|
||||||
|
},
|
||||||
|
"description": "Stop a 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 subagent's name, or all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"offered": "main"
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "view_image",
|
"name": "view_image",
|
||||||
"scope": "cli",
|
"scope": "cli",
|
||||||
|
"sessions": "agent",
|
||||||
"risk": "read",
|
"risk": "read",
|
||||||
"exclusive": false,
|
"exclusive": false,
|
||||||
"block": "read",
|
"block": "read",
|
||||||
|
|||||||
Loaded 100 of 253 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user