Harness spec 3.2.0: effort levels, the thinking switch, parallel capacity

models/schema.json: thinking_switch (the server turns thinking off for
chat_template_kwargs.enable_thinking: false) and capacity.parallel
(single_session is parallel 1). The level `off` exists only with the
switch and sends enable_thinking false and no effort; a model that takes
no efforts has the one level `default`, which sends nothing. The levels a
chat offers are (off) + efforts; one level shows no control, [off, X] a
Thinking switch, more a slider. A start level and a level the model does
not offer follow one rule. quirks.json: thinking-default; acp.md: effort
values are levels. New conformance area: levels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-09 08:07:14 +00:00
1 parent a97c5b46a4
commit fdfcf3666a
5 files changed
+353 -9

No files matched your search

+3 -3
View File
@@ -112,7 +112,7 @@ Params: `{ cwd: <absolute>, mcpServers?: [], _meta?: { mode?, lembas_model?, mod
`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; `model` a ref the device knows (an editor's
choice); `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, instance } } }`.
`instance` (spec 3.0.0) is a boolean, with the `model` event's meaning: true exactly when the
@@ -284,7 +284,7 @@ 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 |
| `steered` | `texts`, `ids?` — the `_lembas/steer` message ids taken in at this step |
| `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) |
| `compacted` | `summary` |
| `title` | `id`, `title` |
@@ -301,7 +301,7 @@ Only on the link (an error on `serve --stdio`).
### `_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> }` between prompts (a level as in `session/new`). Result
`{ model: <ref>, effort }`. Emits a `model` event when something changed.
### `_lembas/steer` — client → agent → result
+337
View File
@@ -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"
}
}
}
]
}
+1 -1
View File
@@ -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.",
"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). A team's members are not refused there: they wait for a slot (team.md, Capacity).",
"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).",
"marks": {
+9 -3
View File
@@ -12,8 +12,9 @@
"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." },
"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)." },
"effort": { "enum": ["minimal", "low", "medium", "high", "xhigh", "max", "off"], "description": "The effort a new conversation starts with; off sends none." },
"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`." },
"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)." },
"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." },
@@ -23,11 +24,16 @@
"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.",
"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." },
"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. A team (team.md) is offered only on a model that serves at least 2." }
},
"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"],
"additionalProperties": true
}
+3 -2
View File
@@ -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.",
"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": "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": "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": "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": "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": "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 }
]
}