diff --git a/harness/README.md b/harness/README.md index 939ffe9..81c18af 100644 --- a/harness/README.md +++ b/harness/README.md @@ -15,14 +15,16 @@ page [Harness-parity](https://git.houmeres.sk/LLeMbas/LLeMbas-CLI/wiki/Harness-p |---|---| | `VERSION` | The spec's own version, apart from LLeMbas CLI's | | `purpose.json` | The `purpose` argument added to a tool whose calls say what they are for | -| `tools/.json` | One tool each: name, `scope`, `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/.json` | One tool each: name, `scope`, `sessions`, `risk`, `exclusive`, `block` (how a call is shown: `shell`, `edit`, `read`, `search`, `web`, `task`, `plan` — not drawn, the plan card refreshes — `hidden`, `tool`), `purpose`, former names (`aliases`, per project), renamed arguments, `variables`, what a project offers of the parameters (`narrow`, per project), `offered` (`subagent`: given to subagents only, never to the main agent — `report`), `requires` and `unavailable` (when it can be offered, and what a call answers when that has gone), `confirm` (`always`: the person is asked in every mode), the description and the JSON Schema of its parameters — exactly what a model is sent | | `prompts/` | The prompt texts; `prompts/index.json` says which are shared, which LLeMbas fragment each one is, and what each `{{variable}}` in a text means. `prompts/personality/` holds the personality presets both offer | | `permission/hardline.json` | The floor: commands refused in every mode, protected paths, and how a command is spelled plainly before it is checked again | | `permission/arity.json` | How much of a command an "always allow" covers | | `permission/defaults.json` | The rules every session starts from | | `permission/modes.json` | The four modes — manual, edit, auto, plan — as what each does with each risk class | | `models/schema.json` | Model metadata: context, output limit, efforts, vision, tools, notes, capacity | -| `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; `context` — result ids, stubs, the output cap, the compaction table | +| `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 | | `quirks.json` | What real servers do, and how both harnesses cope | | `conformance/.json` | Cases with their expected results, run by both test suites | | `acp.md` | The link between them: ACP plus every `_lembas/*` method, field and capability | @@ -35,6 +37,20 @@ page [Harness-parity](https://git.houmeres.sk/LLeMbas/LLeMbas-CLI/wiki/Harness-p - **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 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. 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. +- **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 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`, @@ -46,6 +62,62 @@ page [Harness-parity](https://git.houmeres.sk/LLeMbas/LLeMbas-CLI/wiki/Harness-p every mode; `plan_submit` only in plan mode, where it is the approval. Their permission key is `plan` (`permission/defaults.json`), which a rule written for the former `todo` key still reaches. +## 3.0.0 (draft) + +`VERSION` says `3.0.0-draft` while the spec is written ahead of both implementations: a SemVer +pre-release, ordered below `3.0.0`, which no stable release of either project carries. It becomes +`3.0.0` when both implement it. **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. 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 (`#a3f2`); a read carries its lines and content hash; an unchanged re-read, or a range + inside an earlier unchanged read, is a stub; 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 reads 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`**: `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`. +- **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 Background work is one thing in both projects: a **job**, owned by a session, is a command run in @@ -126,7 +198,9 @@ narrower tool's facts through its `variables`. ## Changing it 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 breaks. 2. A fix comes with a **conformance case** that fails without it. In LLeMbas CLI, diff --git a/harness/VERSION b/harness/VERSION index 227cea2..28a2a86 100644 --- a/harness/VERSION +++ b/harness/VERSION @@ -1 +1 @@ -2.0.0 +3.0.0-draft diff --git a/harness/acp.md b/harness/acp.md index e87a564..1ec3edb 100644 --- a/harness/acp.md +++ b/harness/acp.md @@ -52,10 +52,11 @@ 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` | | `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 | -| `library` | `library` in `session/new` and `session/prompt` `_meta.lembas` is read; `_meta.lembas.scope` sent on the instance library's `tools/call` | -| `prototypes` | `prototypes` in `session/new` and `session/prompt` `_meta.lembas` is read | +| `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. 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` | | `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 | ## Standard ACP, as used here @@ -112,7 +113,19 @@ Params: `{ cwd: , mcpServers?: [], _meta?: { mode?, lembas_model?, mod `lembas_model` is the instance's name of a model (it becomes `/` here); `model` a ref of the device's own; `effort` a level or `"off"`. -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, 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 is opened from the store under the same rules (capability `sessions`). With capability `commands`, an `available_commands_update` follows the answer. @@ -375,6 +388,18 @@ in that summary — and answers `{ reverted: }`. **Files are not 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. +### `_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 Params `{ sessionId }`. Result `{ summary }`. `-32602` while it works. @@ -553,6 +578,14 @@ Besides `/v1/models`, the chat completions, `/api/v1/instance` and `/mcp`: 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. +- **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` (`memory`, and + the resource holding the entries for the prompt). A call made for a session the client opened + carries `params._meta.lembas.scope: ` 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. Local memory files are not + synced up; logged out, there is no memory. + ## Notes - `_lembas/files/search`'s `root` is the session's working directory, which `@path` mentions diff --git a/harness/prompts/blocks/context.md b/harness/prompts/blocks/context.md new file mode 100644 index 0000000..92cee18 --- /dev/null +++ b/harness/prompts/blocks/context.md @@ -0,0 +1,9 @@ + +# Tool results +- Every tool result starts with its id in brackets, like `[#a3f2]`. A read's bracket also says the path, the lines shown, the file's length and a short hash of its content: `[#a3f2 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 #a3f2, 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 #f001.` in the same way. +- Long output is shortened to its beginning and its end, with `[1234 lines elided, full output #c9d0]` 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. diff --git a/harness/prompts/blocks/workspace.md b/harness/prompts/blocks/workspace.md new file mode 100644 index 0000000..75f6593 --- /dev/null +++ b/harness/prompts/blocks/workspace.md @@ -0,0 +1,10 @@ + +# 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}} diff --git a/harness/prompts/index.json b/harness/prompts/index.json index 0eae6e5..a99d073 100644 --- a/harness/prompts/index.json +++ b/harness/prompts/index.json @@ -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)", "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", - "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_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", @@ -183,25 +183,21 @@ "llembas_text": "identical" }, "blocks/prototypes.md": { - "scope": "shared", - "llembas": [ - "core.prototypes" - ], - "llembas_text": "identical", + "scope": "cli", + "llembas": [], "variables": { "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" - } + }, + "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": { - "scope": "shared", - "llembas": [ - "core.library" - ], - "llembas_text": "identical", + "scope": "cli", + "llembas": [], "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" - } + }, + "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": { "scope": "shared", @@ -227,6 +223,23 @@ "llembas": [], "llembas_text": "identical", "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" + } } } }