Harness spec 3.0.0-draft: link protocol 3; one server names the models

acp.md *Versions*: LLeMbas 2.0 and LLeMbas CLI 2.0 speak link protocol 3 and
nothing else, so a 1.x peer and a 2.0 one refuse each other and each says what
to update. A 2.0 server's discovery says `protocol: 3, protocols: [3]` —
`protocol` was frozen at 1 so 1.x clients comparing it could log in; raising
it now is the point. A 2.0 CLI logs in and links only where 3 is listed and
otherwise names the server's version and protocols; a 2.0 server closes a
hello of 1 or 2 with 4400 and "update LLeMbas CLI to 2.0".

Model naming: the served id is the device's ref (one server, no connection
prefix, no second login); the older forms still resolve. A configure naming a
model the device cannot run is -32602 too, and the session keeps its model.
initialize's example says 3; `show` in the client's capabilities.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-10-27 21:18:37 +00:00
1 parent a73adc01f0
commit d5043ab0db
2 files changed
+43 -38

No files matched your search

+4 -1
View File
@@ -108,7 +108,10 @@ this design.
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
- **`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`.
+39 -37
View File
@@ -13,24 +13,31 @@ change to the protocol is a change here first, then in both projects.
## Versions and discovery
- **`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`.
- **Discovery** (`/.well-known/lembas.json`) says `protocol: 1` **and** `protocols: [1, 2]` —
`protocol` stays 1 on purpose, because a client that compares it for equality at login must
still get in. The CLI reads `protocols` (falling back to `protocol` where it is absent) at
login and again **every time the link dials**, keeps it with the login, and says protocol 2 in
`hello` and `initialize` only to an instance that lists 2; to any other it says 1 (an instance that
speaks only 1 closes a link whose hello says 2 with **4400**). A link closed with 4400 stops — it is not
redialled — saying *the instance and this CLI speak different link protocols — update one of them*.
- **A protocol 1 peer keeps working.** Every feature beyond protocol 1 is 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**. The CLI logs in to an instance whose discovery says
protocol 1 or 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
the server's file system, memory and models, and a 2.0 CLI talks to nothing else, so a 1.x peer
and a 2.0 one **refuse each other**, each saying what to update:
- **Discovery** (`/.well-known/lembas.json`) of a 2.0 server says `protocol: 3` **and**
`protocols: [3]`. `protocol` was frozen at 1 for 1.x so that a client comparing it for equality
could still log in; 3 raises it on purpose, so a 1.x CLI's login is refused there (it says the
server speaks protocol 3 and to update the older of the two).
- A **2.0 CLI** logs in only where discovery lists 3 (`protocols`, falling back to `protocol`).
To a server that does not, at login and again at every dial of the link, it says *&lt;server&gt;
runs LLeMbas X, which speaks link protocol 1 and 2; this LLeMbas CLI needs a LLeMbas 2.0 server
(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
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
`_lembas/question` and `_lembas/plan`, and one that then answers them with method-not-found is
treated as protocol 1 for the rest of the link.
`_lembas/question` and `_lembas/plan` (a 2.0 server says 3), and one that then answers them with
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 |
|---|---|
@@ -63,7 +70,7 @@ change to the protocol is a change here first, then in both projects.
### `initialize` — client → agent
Params: `{ protocolVersion: 1, clientCapabilities: { _meta?: { lembas?: { protocol: 2 } | true } } }`.
Params: `{ protocolVersion: 1, clientCapabilities: { _meta?: { lembas?: { protocol: 3, show?: true } | true } } }`.
Result:
@@ -76,10 +83,10 @@ Result:
"mcpCapabilities": { "http": false, "sse": false }
},
"authMethods": [],
"agentInfo": { "name": "LLeMbas CLI", "version": "1.3.0" },
"agentInfo": { "name": "LLeMbas CLI", "version": "2.0.0" },
"_meta": { "lembas": {
"protocol": 2,
"harness_spec": "1.1.0",
"protocol": 3,
"harness_spec": "3.0.0",
"limits": { "roots": ["~/work"], "max_mode": "edit" },
"connection": "example",
"capabilities": ["directories", "configure", "…"]
@@ -88,30 +95,24 @@ Result:
```
`limits` is `null` on `serve --stdio`. `connection` is the device's name for the instance — the
login's connection — or `null`; an instance that does not speak protocol 2 recognises its own
models by it, as `<connection>/<name>`, while the `model` event and `announce` say it directly
(`instance`).
login's connection — or `null`; the `model` event and `announce` say directly whether a model is
the instance's (`instance`).
**How the device names an instance's models.** An instance that speaks protocol 2 serves every model
as `<provider>/<model>` (`deepseek/deepseek-flash`), and that served id **is the device's ref** for
it — each provider is, as the person sees it, a connection of its own, backed by the instance
(requests still go to its `/v1`, with the served id). Exceptions: a connection of the device's own
with the provider's name keeps the short form, and the instance's model is then
`<login>/<provider>/<model>`; with two instances serving one provider, the one logged in to first
keeps the short form and the other's are `<login>/<provider>/<model>`; an instance that does not
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.
**How the device names an instance's models.** The served id **is the device's ref** for a model
(`deepseek/deepseek-flash`; spec 3.0.0: a 2.0 device talks to one server, the one it is logged in
to, so there is nothing to tell apart — no connection prefix, no second login, no connection of
the device's own). The older forms (`<login>/<id>`, and from before providers `<login>/<model>` for
the one served `<provider>/<model>` of that name) 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.
`model` in a `model` event and in a result is the device's ref, which is the served id.
### `session/new` — client → agent
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`
(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`
a ref of the device's own; `effort` a level or `"off"`.
`lembas_model` is the instance's name of a model; `model` a ref the device knows (an editor's
choice); `effort` a level or `"off"`.
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
@@ -121,7 +122,8 @@ session's model is one this link's instance serves.
**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
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
@@ -597,4 +599,4 @@ Besides `/v1/models`, the chat completions, `/api/v1/instance` and `/mcp`:
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
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*).