chore: update docs + rename reset to clear

This commit is contained in:
can1357
2026-08-03 18:27:05 +02:00
parent 01c1f91ff5
commit 1a8caad23e
68 changed files with 1217 additions and 326 deletions
+16
View File
@@ -116,6 +116,20 @@ Tools are passed in the top-level `tools` array. Each user-defined (client) tool
Anthropic-schema client tools (`bash`, `text_editor`, `computer`, `memory`) and server tools (`web_search`, `web_fetch`, `code_execution`, `tool_search`) instead carry a versioned `type`, e.g. `{"type": "web_search_20250305", "name": "web_search"}`.
### OMP native-adapter schema normalization
The native Anthropic provider does not forward a pi tool's JSON Schema unchanged. Before placing it in `input_schema`, OMP keeps these keywords:
- on every node: `$ref`, `$defs`, `$schema`, `definitions`, `type`, `enum`, `const`, `description`, `title`, `default`, and `nullable`;
- nested `anyOf` and `allOf` (root combinators are not retained, and `oneOf` is not retained at any depth);
- on objects: `properties`, `required`, and `additionalProperties`;
- on arrays: `items`, `prefixItems`, and `minItems` only when it is `0` or `1`;
- on strings: `format` only for `date-time`, `time`, `date`, `duration`, `email`, `hostname`, `uri`, `ipv4`, `ipv6`, or `uuid`.
Other constraints—including `pattern`, string-length limits, numeric ranges, `maxItems`, unsupported formats, and unsupported combinators—are appended to that node's `description`. They remain model-visible guidance but are no longer machine-enforced schema keywords. Object nodes default to `additionalProperties: false`; an explicit `true` or schema-valued `additionalProperties` remains open (an empty schema normalizes to `true`).
OMP sends `strict: true` only for eligible built-in tools (`bash`, `python`, `edit`, and `find`) when neither `PI_NO_STRICT` nor provider compatibility/runtime fallback has disabled strict tools, the tool has not opted out, the raw schema avoids `oneOf`, `allOf`, `$ref`, `patternProperties`, and `propertyNames`, and every object is closed. Selection is capped at 20 strict tools per request and shares budgets of 24 optional properties and 16 union uses: after the optional budget is exhausted, another optional property must be converted to required-and-nullable using union budget or that tool remains non-strict. Other tools use the normalized non-strict schema. OMP sends `eager_input_streaming: true` only when the model compatibility data and effective endpoint support it: first-party Anthropic endpoints qualify, as do custom endpoints explicitly configured for that capability; a canonical model rerouted to an unqualified non-Anthropic endpoint does not.
`tool_choice` controls invocation (four options):
- `{"type":"auto"}` — model decides (default when `tools` present).
- `{"type":"any"}` — must call some tool.
@@ -196,6 +210,8 @@ OMP operates on the underlying prompt-driven XML rather than Messages API conten
The scanner mints call ids because this XML has none. It scans streamed text statefully, emits `toolArgDelta` events while each parameter body arrives, and publishes the coerced argument object with `toolEnd` after `</invoke>`. A parameter value is capped at 1,000,000 JavaScript string code units; overflow gains an explicit truncation suffix. JSON-like values are parsed with repair, while schema-declared strings stay strings. With `parseThinking: true`, `<thinking>`, `<think>`, and `<scratchpad>` (prefixed or unprefixed) become thinking events; otherwise those tags remain visible text.
`</invoke>` gates `toolEnd`, but it does not gate creation of the canonical call. Once an opening `<invoke name="…">` has emitted `toolStart`, EOF only resets scanner-local state. On a normally stopped stream, OMP retains that call, changes the turn to `toolUse`, and may dispatch it even though no `toolEnd` arrived. Any `toolArgDelta` text already accumulated survives in the call (without the close-time coercion); a call with no accumulated parameter text runs with `{}`. A `length` stop remains non-runnable.
---
## Multiple / parallel tool calls
+11 -7
View File
@@ -384,13 +384,17 @@ The current scanner accepts all three forms described above:
- fullwidth or ASCII DSML `invoke` / `parameter` blocks.
For V3.1 and legacy calls, omp emits `toolStart` after the header is complete
but buffers arguments until `</|tool▁call▁end|>`; it then uses the shared
repairing JSON parser. A missing/invalid argument object becomes `{}`, and an
unfinished call is discarded on flush. DSML is genuinely incremental:
`toolArgDelta` events are emitted while each parameter body arrives. A DSML
parameter is a raw string unless `string="false"`; the latter is
repairing-JSON-decoded and falls back to the raw text if decoding fails. Call
IDs for id-less DeepSeek forms are synthesized as `ptc_…`.
but buffers arguments until `<|tool▁call▁end|>`; it then uses the shared
repairing JSON parser. A missing/invalid completed argument object becomes
`{}`. Flush emits no `toolEnd` for an unfinished call and only clears the
scanner's private state. Once `toolStart` has been projected, however, the
canonical call remains and a normally stopped turn may dispatch it: unfinished
V3.1/legacy calls retain `{}`, while DSML calls retain any argument text already
published through `toolArgDelta`. DSML is genuinely incremental: parameter
body text is streamed as those deltas. A DSML parameter is a raw string unless
`string="false"`; the latter is repairing-JSON-decoded at a completed close and
falls back to the raw text if decoding fails. Call IDs for id-less DeepSeek
forms are synthesized as `ptc_…`.
The scanner also removes leaked DeepSeek chat-template control tokens from
visible text and, by default, maps `<think>…</think>` to thinking events. Its
+2 -1
View File
@@ -125,7 +125,7 @@ It's currently 11.4°C in London.
## OpenAI-compatible / native API mapping
- Hosted Gemini's native API normally returns a structured `functionCall` part (`{name, args}`); for Gemini 3 each carries an `id` that must be echoed in the matching `functionResponse`, plus a `thoughtSignature` that must be preserved. The Pythonic text form is what you get when the structured path *fails* (`finish_reason = MALFORMED_FUNCTION_CALL`) or when tool use is driven purely by prompt (Gemma, or Gemini via the code-execution `executableCode` part).
- Hosted Gemini's native API normally returns a structured `functionCall` part (`{name, args}`). On direct Gemini Generative AI requests, Gemini 3 calls carry an `id` that OMP echoes in the matching `functionResponse`; their `thoughtSignature` must also be preserved. OMP's Vertex adapter is the exception: Vertex GenerateContent rejects function-part IDs, so OMP omits `id` from both `functionCall` and `functionResponse`, retains the originating function name, and relies on function name/order for matching. Thought signatures are still preserved.
- When parsed out of an OpenAI-compatible shim, each recovered call becomes `tool_calls[i] = {id (server-minted), type:"function", function:{name, arguments:<JSON string>}}` — the Python kwargs are re-serialized to a JSON string at that boundary.
- Feed results back as the deployment's tool/`functionResponse` turn (hosted) or a `tool_outputs` block in the next user turn (prompt-driven).
@@ -139,6 +139,7 @@ It's currently 11.4°C in London.
- **OMP streaming behavior.** The scanner buffers the entire `tool_code` body and emits tool events only after the closing fence; it does not stream partial arguments. An unterminated block is discarded on flush rather than exposed as text. Positional arguments and malformed keyword segments are skipped. In addition to ordinary quoted strings, the literal decoder accepts Python raw/byte/unicode prefixes, triple quotes, octal escapes, and `\x`/`\u`/`\U` escapes.
- **Transcript rendering.** OMP wraps the transcript in `<bos>` and Gemma-style `<start_of_turn>user|model` turns. `developer` text is prepended to the next user turn (or emitted as its own user turn if no user message follows); consecutive tool results become one user turn containing their separate `tool_outputs` blocks.
- **Variant divergence.** Gemma **4** abandoned this Pythonic form for a token-delimited brace syntax (`<|tool_call>call:NAME{…}<tool_call|>`) — a different convention documented in `gemma.md`. This spec covers hosted Gemini and Gemma 3.
- **Gemma 3 automatic-selection caveat.** OMP's current family affinity maps every recognized Gemma version—including Gemma 3—to the `gemma` dialect. Therefore, when a Gemma 3 model is marked `supportsTools: false` and falls back from native tools, `tools.format=auto` selects the incompatible Gemma 4 grammar. Set `tools.format=gemini` explicitly for the Pythonic Gemma 3 convention documented here.
## Sources
+12 -11
View File
@@ -11,11 +11,11 @@ Gemma 4 wraps each structural element in a paired token. Note the **asymmetric p
| Open | Close | Purpose |
|---|---|---|
| `<bos>` | — | Beginning of sequence |
| `<|turn>` | `<turn|>` | One conversation turn; the role name is the first line of the body |
| `<|tool_call>` | `<tool_call|>` | One tool **call** emitted by the model |
| `<|tool_response>` | `<tool_response|>` | One tool **result** fed back to the model |
| `<|channel>` | `<channel|>` | Reasoning channel; `<|channel>thought` opens the model's chain-of-thought (closed by `<channel|>`) before the visible reply |
| `<|"|>` | `<|"|>` | String-literal delimiter (same token on both ends) |
| `<\|turn>` | `<turn\|>` | One conversation turn; the role name is the first line of the body |
| `<\|tool_call>` | `<tool_call\|>` | One tool **call** emitted by the model |
| `<\|tool_response>` | `<tool_response\|>` | One tool **result** fed back to the model |
| `<\|channel>` | `<channel\|>` | Reasoning channel; `<\|channel>thought` opens the model's chain-of-thought (closed by `<channel\|>`) before the visible reply |
| `<\|"\|>` | `<\|"\|>` | String-literal delimiter (same token on both ends) |
| `<eos>` | — | End of sequence |
Because the string delimiter is a token (`<|"|>`), values may contain raw ASCII quotes and commas without escaping — only a literal `<|"|>` token sequence cannot appear inside a string.
@@ -28,7 +28,7 @@ Each turn is `<|turn>{role}\n{body}<turn|>`, and turns are concatenated with no
## Tool definitions
The `gemma` dialect does not put tool schemas on the wire. Tools are advertised in the system prompt by `renderInbandToolPrompt` (`packages/ai/src/dialect/catalog.ts`): an OpenAI-style JSON catalog — one object per line inside a `<tools></tools>` block — followed by the format guide (`packages/ai/src/dialect/gemma.md`):
The owned `gemma` prompt **does** carry each tool's normalized wire schema. `renderInbandToolPrompt` serializes one compact OpenAI-style object per line inside `<tools></tools>`, followed by the Gemma format guide:
```text
<tools>
@@ -36,7 +36,7 @@ The `gemma` dialect does not put tool schemas on the wire. Tools are advertised
</tools>
```
The verbose system-prompt inventory and `/dump` additionally render each tool as a `# Tool: <name>` section — description, a TypeScript-style parameter signature, and native `<|tool_call>` examples — via `renderToolInventory` (`packages/ai/src/dialect/inventory.ts`).
`renderToolInventory` is a separate verbose inventory used by the system prompt and `/dump`. It emits one `## functions` TypeScript `namespace functions { … }` block. Tool descriptions are `//` comments above `type NAME = (_: PARAMS);` declarations; configured examples appear as JSDoc-style `// @example` entries whose calls use Python keyword-argument syntax. It does not emit per-tool Markdown sections or native Gemma `<|tool_call>` examples.
## Tool-call format
@@ -50,12 +50,12 @@ Value grammar inside `{…}`:
| Value kind | Encoding | Example |
|---|---|---|
| string | `<|"|>text<|"|>` | `location:<|"|>London<|"|>` |
| string | `<\|"\|>text<\|"\|>` | `location:<\|"\|>London<\|"\|>` |
| int / float | bare | `count:42` |
| bool | bare | `flag:true` |
| null | bare | `unit:null` |
| list | `[v,v,…]` | `tags:[<|"|>a<|"|>,<|"|>b<|"|>]` |
| nested object | `{k:v,…}` | `config:{theme:<|"|>dark<|"|>}` |
| list | `[v,v,…]` | `tags:[<\|"\|>a<\|"\|>,<\|"\|>b<\|"\|>]` |
| nested object | `{k:v,…}` | `config:{theme:<\|"\|>dark<\|"\|>}` |
The OMP parser is the streaming `GemmaInbandScanner` (`packages/ai/src/dialect/gemma.ts`), not a flat regex. For each `<|tool_call>` block it:
@@ -97,8 +97,9 @@ The current weather in Tokyo is 15 degrees Celsius and sunny.<turn|>
- **Asymmetric pipes.** The closer is `<tool_call|>`, not `</tool_call>` or `<|tool_call>`. Matching the wrong pipe side will never close the block.
- **One call per block.** Unlike a JSON `tool_calls[]` array, parallelism is "more blocks", not "more entries in one block".
- **Bare scalars.** A value not wrapped in `<|"|>` is `true`/`false` → bool, `null`/`none` → null, numeric → number, otherwise a bare string (e.g. an unquoted enum or type name like `STRING`).
- **Tool-call ids are synthesized.** The format carries no id; OMP mints one when the scanner opens each call block and correlates rendered responses by the surrounding message order/name.
- **Tool-call ids are synthesized.** The format carries no id; after receiving a complete closed block, OMP parses it and emits adjacent `toolStart`/`toolEnd` events with a newly minted id. Rendered responses are correlated by surrounding message order/name.
- **Not Gemma 3 / hosted Gemini.** Those use the Pythonic `tool_code` / `default_api` form in `gemini.md`. Gemma 4 replaced it with this token syntax; the two are not interchangeable.
- **Gemma 3 automatic-selection caveat.** OMP's current family affinity maps Gemma 3 and Gemma 4 model IDs to `gemma`. If a Gemma 3 model is marked `supportsTools: false`, `tools.format=auto` therefore chooses this Gemma 4 grammar even though Gemma 3 requires the Pythonic convention in `gemini.md`; set `tools.format=gemini` explicitly.
## Sources
+11 -6
View File
@@ -307,12 +307,17 @@ instead.
The scanner synthesizes `ptc_…` ids, emits `toolStart` once the name delimiter
arrives, and streams each argument body as keyed `toolArgDelta` events.
String-only schema properties stay verbatim; every other property is parsed
with strict `JSON.parse` after trimming and falls back to the original raw text
on failure. An unfinished key/value drops the open call on flush. The scanner
also heals narrowly recognizable model mistakes: `</arg_key>` used in place
of `</arg_value>`, a stray wrong closer before the real closer, and a missing
value closer immediately before the next argument or call close.
String-only schema properties stay verbatim; every other completed property is
parsed with strict `JSON.parse` after trimming and falls back to the original
raw text on failure. On flush, an unfinished key/value drops only the scanner's
private call state. If `toolStart` was already emitted, OMP retains the
canonical call and a normal stop may dispatch it; previously accumulated
arguments—including partial value text published through `toolArgDelta`—remain
on that call. Input that never yields a valid name emits no `toolStart` and
therefore leaves no call. The scanner also heals narrowly recognizable model
mistakes: `</arg_key>` used in place of `</arg_value>`, a stray wrong closer
before the real closer, and a missing value closer immediately before the next
argument or call close.
Thinking parsing is enabled by default and excludes `<think>…</think>` from
visible text. If `<tool_response>` appears in assistant output, the scanner
+4 -1
View File
@@ -132,6 +132,8 @@ OMP emits the first form above: no `<|constrain|>` marker, recipient in the chan
Arguments are accumulated until `<|call|>`, `<|end|>`, or `<|return|>` and parsed with JSON repair. Empty arguments, or input that still cannot be parsed after repair, become `{}` rather than a scanner error. The scanner emits `toolStart` when the header completes and `toolEnd` only at the message terminator; `analysis` body chunks stream as thinking deltas, while ordinary assistant `commentary`/`final` bodies stream as text. Non-assistant messages, including tool-result envelopes, are skipped by this output scanner.
An important owned-scanner edge case differs from canonical Harmony. After a recipient-bearing header reaches `<\|message\|>`, OMP has already emitted `toolStart`. If the ordinary streaming path drains the body bytes and the stream then ends without `<\|call\|>`, `<\|end\|>`, or `<\|return\|>`, `flush()` emits no `toolEnd` and does not retract the start. The Harmony scanner emits no argument deltas, so the retained canonical call still has `{}` even if unterminated body text was seen. On a normal stop, OMP changes the turn to `toolUse` and may dispatch that empty call. This is permissive and unsafe recovery behavior, not a valid Harmony terminator rule.
## Multiple / parallel tool calls
Harmony has no special "parallel" wrapper. Multiple calls are just multiple consecutive messages. The model may first emit an optional **preamble** — a *user-visible* assistant message on the `commentary` channel (unlike `analysis`, this is meant to be shown) — then one tool-call message per function. Each individual call still ends with its own `<|call|>` stop token, so a host that stops on `<|call|>` collects calls one at a time, executes, feeds the result back, and resumes:
@@ -207,7 +209,8 @@ When a server (vLLM/SGLang/Ollama) bridges Harmony to Chat Completions JSON:
- **Tool result messages** (`{"role":"tool","tool_call_id":...,"content":...}`) are rendered into `<|start|>{toolname} to=assistant<|channel|>commentary<|message|>{content}<|end|>`. The server maps `tool_call_id` → the original function name to build the `{toolname}` author.
- **Reasoning**: `analysis`-channel text is surfaced as `reasoning_content` (vLLM/SGLang) or as a `reasoning`/`thinking` field, and is generally not echoed back on subsequent requests. `final`-channel text is the normal `message.content`. `commentary` preambles, if surfaced, also map to assistant content.
- **OMP transcript rendering:** `developer`, `user`, and other non-assistant roles map directly to Harmony envelopes. Assistant messages emit, in order, a complete `analysis` message for thinking, a complete `final` message for visible text, then one `commentary` call message per tool call. Thus visible text accompanying a tool call is rendered as `final`, not as a commentary preamble. Tool-result runs become consecutive canonical tool-author envelopes.
- **`tools` / `tool_choice`** request fields are compiled by the chat template into the developer-message `namespace functions { ... }` block; the system message gains the commentary-routing line.
- **Native server/chat-template compilation:** on the native vLLM/SGLang path, request `tools` / `tool_choice` are compiled by the server's chat template into the developer-message `namespace functions { ... }` block; the system message gains the commentary-routing line.
- **OMP owned-dialect advertisement:** when OMP's `harmony` dialect is selected, OMP removes native provider tools and appends its generic compact `<tools>` JSON catalog plus the Harmony format guide to the system prompt. This path does not use the canonical developer-message namespace as its tool advertisement.
## Parsing notes & gotchas
+10 -8
View File
@@ -196,14 +196,16 @@ tool-result messages are collapsed into one synthetic user message containing
that text.
The scanner recognizes only calls inside a section. Once the argument marker
arrives it preserves the raw header as the call id and derives the name from
the last dot-separated segment before the first colon. It emits `toolStart` at
that point, buffers the argument body until `<|tool_call_end|>`, then applies
the shared repairing JSON parser and emits `toolEnd`; it does **not** emit
incremental argument deltas. Invalid/non-object arguments normalize to `{}`,
and an unfinished call is discarded on flush. Section markers are suppressed
from visible text, while an isolated call marker outside a section remains
ordinary text.
arrives it preserves the raw header as the call id, derives the name from the
last dot-separated segment before the first colon, and emits `toolStart`. It
buffers the argument body until `<|tool_call_end|>`, then applies the shared
repairing JSON parser and emits `toolEnd`; it does **not** emit incremental
argument deltas. Invalid/non-object completed arguments normalize to `{}`.
If EOF arrives after `toolStart` but before the close marker, no `toolEnd` is
emitted, yet the canonical `{}` call remains and may be dispatched on a normal
stop. Only incomplete input that never reaches the argument marker is
discarded without creating a call. Section markers are suppressed from visible
text, while an isolated call marker outside a section remains ordinary text.
Thinking parsing is enabled by default and maps `<think>…</think>` to thinking
events. `parseThinking: false` leaves those tags and their contents in visible
+211
View File
@@ -0,0 +1,211 @@
# MiniMax owned tool-calling format (`<minimax:tool_call>`)
OMP's `minimax` dialect is the prompt-driven, in-band tool protocol for MiniMax-family models. Calls are ordinary assistant text: one `<minimax:tool_call>` envelope contains one or more `<invoke>` elements. OMP executes the parsed calls and returns a `<function_results>` block in the next user turn. The format carries no tool-call ids, so calls and results are correlated by order.
This reference describes OMP's implemented converter, not MiniMax's provider-native structured tool API. It is verified against `packages/ai/src/dialect/minimax.ts`, the shared XML scanner in `packages/ai/src/dialect/anthropic.ts`, prompt assembly in `packages/ai/src/dialect/catalog.ts`, and the streaming projection in `packages/ai/src/dialect/owned-stream.ts`.
## Selection and request conversion
Set the format explicitly in `~/.omp/agent/config.yml` or a project/overlay config:
```yaml
tools:
format: minimax
```
`tools.format: minimax` forces this owned dialect for the session. In `auto` mode, OMP keeps provider-native tool calling unless the selected model explicitly has `supportsTools: false`; for a MiniMax-family model id, that fallback resolves to `minimax`. See [`tools.format`](../settings.md#tools-and-approvals).
When an owned dialect is active, OMP:
1. removes the native structured `tools` field from the provider request;
2. appends an in-band tool catalog and the MiniMax format guide to the system prompt;
3. rewrites prior structured assistant calls and tool-result messages into this text protocol; and
4. scans the model's text stream back into structured tool-call events.
## Tool definitions and prompt injection
The injected prompt begins with `# Tools`, says calls are text rather than native provider tool messages, and lists the available functions inside `<tools></tools>`. Each line is a compact OpenAI-style function object containing the normalized wire schema:
```text
<tools>
{"type":"function","function":{"name":"read","description":"Read a file","parameters":{"type":"object","properties":{"path":{"type":"string"},"count":{"type":"number"}},"required":["path"]}}}
</tools>
```
The catalog is followed by the MiniMax-specific guide from `packages/ai/src/dialect/minimax.md`. Its contract requires a listed function name, literal string/scalar bodies, JSON lists/objects, one envelope for a batch, and no model-authored result blocks.
## Tool-call envelope
A single call is:
```text
<minimax:tool_call>
<invoke name="read"><parameter name="path">src/main.ts</parameter><parameter name="count">40</parameter></invoke>
</minimax:tool_call>
```
Exact structure:
| Element | Meaning |
| --- | --- |
| `<minimax:tool_call>…</minimax:tool_call>` | Required model-output envelope in the prompt contract. |
| `<invoke name="TOOL">…</invoke>` | One call. `name` must be a listed tool. |
| `<parameter name="ARG">VALUE</parameter>` | One named argument. Arguments occur directly inside the invoke. |
The renderer XML-escapes tool and argument names in attributes. Parameter bodies are deliberately **not** XML-escaped: this protocol is delimiter-matched rather than parsed as XML. For example, a string body is `a & b < c`, not `a &amp; b &lt; c`. A literal `</parameter>` is the one reserved sequence because it closes that argument.
The scanner is more tolerant than the prompt contract. It accepts the namespaced wrapper above, an unprefixed `<tool_call>` wrapper, or a bare `<invoke>` outside a wrapper. Models should still emit the canonical `<minimax:tool_call>` form so behavior does not depend on recovery paths.
## Argument encoding and coercion
Encoding uses the selected tool's schema:
| Declared/value kind | Rendered parameter body | Parsed value |
| --- | --- | --- |
| Schema-declared string whose runtime value is a string | Verbatim text, including leading/trailing spaces and newlines | Verbatim string |
| Number, boolean, `null`, array, or object | JSON | Parsed JSON value |
| Value without a matching string schema | JSON, including quotes around a string | Parsed JSON when valid |
Example:
```text
<invoke name="write"><parameter name="path">notes/a & b.txt</parameter><parameter name="options">{"append":false,"tags":["x","y"]}</parameter></invoke>
```
The scanner resolves string arguments from the supplied tool schemas. A parameter attribute can override that decision:
- `string="true"` (and any value except `false`, `0`, or `no`) forces verbatim string handling.
- `string="false"`, `string="0"`, or `string="no"` forces JSON parsing even for a schema-declared string.
For a non-string parameter, surrounding whitespace is trimmed only for the JSON parse. OMP uses its repair-capable JSON parser; if parsing still fails, the original body is retained as a string rather than dropping the argument. Empty bodies also remain empty strings. A parameter with no usable `name` is ignored.
## Multiple and parallel calls
Parallel calls are sibling `<invoke>` elements inside one envelope, in emitted order:
```text
<minimax:tool_call>
<invoke name="read"><parameter name="path">src/a.ts</parameter></invoke>
<invoke name="read"><parameter name="path">src/b.ts</parameter></invoke>
</minimax:tool_call>
```
The scanner mints an internal id for each invoke because the wire format has no id. OMP can dispatch the resulting calls as a batch. Tool results must be returned in the same order; the result protocol has no call id with which to repair reordering.
## Tool-result envelope
OMP batches consecutive tool results into one `<function_results>` block. Success and failure use different records:
```text
<function_results>
<result>
<tool_name>read</tool_name>
<stdout>file contents</stdout>
</result>
<error>
<tool_name>read</tool_name>
<stderr>ENOENT: file not found</stderr>
</error>
</function_results>
```
For every result:
- success is `<result>` with `<stdout>`;
- `isError: true` is `<error>` with `<stderr>`;
- `<tool_name>` is XML-text escaped;
- stdout/stderr is inserted verbatim; and
- there is no call id, so the model reads records in call order.
OMP places this text in a synthesized `user` message. Text blocks from one tool result are concatenated; image result blocks remain image blocks after the rendered text. The model must never emit `<function_results>` or `<tool_response>` itself.
## Thinking and visible text
OMP renders a preserved reasoning block as:
```text
<thinking>
reasoning text
</thinking>
```
In the normal owned-tool stream, thinking parsing is enabled. The MiniMax scanner recognizes `<thinking>`, `<think>`, and `<scratchpad>` (including the supported prefixed forms), emits separate thinking events, and keeps the content out of visible assistant text. If `parseThinking` is disabled for a direct scanner consumer, those tags remain visible text. An unterminated thinking block is closed logically on stream flush and its accumulated content is retained.
Visible prose may precede the tool envelope. Text outside calls remains assistant text; non-call text inside the wrapper is discarded by the scanner.
## Streaming, malformed output, and recovery
The scanner is incremental and chunk-boundary safe: opening/closing tags and parameter bodies may arrive in separate provider deltas. Its observable lifecycle is:
1. a non-empty `<invoke name="…">` emits `toolStart` immediately;
2. each named parameter body emits keyed `toolArgDelta` events as text chunks arrive; and
3. the matching `</invoke>` performs final coercion and emits `toolEnd` with the complete arguments and exact raw invoke block.
Important failure behavior:
- **Missing call name:** no tool lifecycle is emitted for that invoke.
- **Missing parameter name:** that parameter is ignored.
- **Malformed JSON:** falls back to the original parameter text.
- **Very large parameter:** input is capped at 1,000,000 JavaScript string code units; overflow is replaced by the accepted prefix plus an explicit truncation marker.
- **Incomplete invoke:** flush resets scanner-local call state and emits no `toolEnd`. However, OMP's stream projector has already materialized a call from `toolStart`; on a normally stopped response it retains that partial call, marks the turn as tool use, and may dispatch it. Already streamed argument text remains uncoerced, and a call with no argument text has `{}`. A provider `length` stop remains `length` rather than becoming runnable tool use.
- **Incomplete wrapper after complete invokes:** already closed invokes remain valid; the wrapper close is not required to emit their `toolEnd` events.
- **Incomplete thinking:** retained as thinking and logically ended at flush.
OMP also guards against a model fabricating tool output after its call. For this dialect, the first `<function_results>` or `<tool_response>` boundary stops projection. With the default `tools.abortOnFabricatedResult: true`, generation is aborted immediately; when disabled, OMP drains the provider stream but discards the fabricated continuation.
## End-to-end example
Injected tool definition (abbreviated to the relevant catalog line):
```text
<tools>
{"type":"function","function":{"name":"get_weather","description":"Get weather","parameters":{"type":"object","properties":{"city":{"type":"string"},"units":{"type":"string"}},"required":["city"]}}}
</tools>
```
Assistant call:
```text
I'll check both cities.
<minimax:tool_call>
<invoke name="get_weather"><parameter name="city">Tokyo</parameter><parameter name="units">celsius</parameter></invoke>
<invoke name="get_weather"><parameter name="city">Oslo</parameter><parameter name="units">celsius</parameter></invoke>
</minimax:tool_call>
```
Next user turn produced by OMP:
```text
<function_results>
<result>
<tool_name>get_weather</tool_name>
<stdout>{"temperature":28,"condition":"clear"}</stdout>
</result>
<result>
<tool_name>get_weather</tool_name>
<stdout>{"temperature":14,"condition":"rain"}</stdout>
</result>
</function_results>
```
The assistant can then answer normally or emit another complete MiniMax call envelope.
## Parsing notes and gotchas
- **Not real XML.** Do not entity-escape parameter bodies or run them through an XML DOM parser; matching is based on protocol delimiters.
- **One envelope, many invokes.** Parallelism is sibling calls inside `<minimax:tool_call>`, not JSON `tool_calls` and not one envelope per required batch.
- **Schema determines strings.** Without the tool schema, even a JavaScript string renderer value is JSON-quoted; supply tool definitions to renderer/scanner APIs for round trips.
- **No ids on the wire.** OMP-generated ids are internal. Preserve call/result order.
- **Errors are first-class records.** Use `<error>/<stderr>`, not a successful `<result>` containing an out-of-band error flag.
- **Canonical wrapper vs accepted recovery syntax.** The parser accepts bare invokes and `<tool_call>`, but the injected contract requires `<minimax:tool_call>`.
- **Complete the invoke before stopping.** A natural-language promise to call a tool is not a call; the closing `</invoke>` is what finalizes coercion and the normal lifecycle.
## Sources
- `packages/ai/src/dialect/minimax.md` — injected MiniMax format guide.
- `packages/ai/src/dialect/minimax.ts` — call, result, thinking, and transcript renderers plus scanner configuration.
- `packages/ai/src/dialect/anthropic.ts` — shared incremental invoke/parameter scanner and coercion behavior.
- `packages/ai/src/dialect/catalog.ts` and `prompt-template.md` — tool catalog and system-prompt injection.
- `packages/ai/src/dialect/history.ts` and `owned-stream.ts` — history conversion, streamed projection, incomplete-call behavior, and fabricated-result boundary.
- `packages/catalog/src/identity/dialect.ts` and `packages/coding-agent/src/sdk.ts` — MiniMax family affinity and `tools.format` resolution.
- `packages/ai/test/inband-tools.test.ts` — prompt rendering, call round trips, chunked argument deltas, raw blocks, MiniMax wrapper recovery, and result rendering.
+16 -8
View File
@@ -124,9 +124,13 @@ idle watchdogs use request options when supplied, otherwise the standard
The initial `start` event is not considered progress for the idle watchdog.
If the SSE connection closes without a terminal event, the client synthesizes
a terminal assistant boundary so `.result()` cannot hang: caller cancellation
becomes an `error` event with `stopReason: "aborted"`; otherwise it becomes an
empty `done` event with `stopReason: "stop"`.
a terminal assistant boundary so `.result()` cannot hang. Caller cancellation
emits `{type:"error", reason:"aborted", error: syntheticAssistant}`; the nested
`AssistantMessage` has `stopReason:"aborted"` and
`errorMessage:"stream closed without terminal event"`. Any other clean close
emits `{type:"done", reason:"stop", message: syntheticAssistant}`, whose nested
message has `stopReason:"stop"`. Thus `reason` is the top-level event field;
`stopReason` exists only on the nested `AssistantMessage`.
The client consumes streaming responses only. The server endpoint also
supports `stream: false`, returning:
@@ -139,7 +143,7 @@ with the full canonical `AssistantMessage` in `message`.
## Errors
Pre-stream HTTP failures use:
Provider/handler failures that reach the pi-native route use:
```json
{ "error": { "type": "rate_limit_error", "message": "..." } }
@@ -147,10 +151,14 @@ Pre-stream HTTP failures use:
with the appropriate HTTP status, `Content-Type: application/json`, and
`Cache-Control: no-store`. The client converts this shape into
`AuthGatewayError`, preserving status, response headers, and `type`. A
nonconforming error body falls back to
`auth-gateway STATUS: BODY_OR_STATUS_TEXT`. A successful response with no body
is also an `AuthGatewayError`.
`AuthGatewayError`, preserving status, response headers, and `type`.
Bearer authentication runs before the route handler. A missing or invalid
gateway bearer is rejected as `{"error":"unauthorized"}` instead of the
structured provider envelope; the client therefore uses its generic
`auth-gateway STATUS: BODY_OR_STATUS_TEXT` fallback and has no provider error
`type` to preserve. Other nonconforming error bodies use the same fallback. A
successful response with no body is also an `AuthGatewayError`.
## Source of truth
+16 -11
View File
@@ -197,22 +197,27 @@ pi tool-call events. `hermes` remains a separate selectable dialect even
though both emit the same basic JSON-in-`<tool_call>` convention.
The catalog's current family-affinity helper maps every model id containing
`qwen` to `qwen3`, including Qwen3-Coder. That broad affinity does not change
the format distinction described below, so callers must explicitly select the
appropriate dialect for Coder endpoints.
`qwen` to `qwen3`, including Qwen3-Coder. For a Coder endpoint, set
`tools.format=native` (or the equivalent native-tool setting) and configure the
serving endpoint itself with its `qwen3_xml` parser. `qwen3_xml` is not an
OMP-owned dialect and therefore is not a valid `tools.format` value.
The omp renderer always writes a nested `arguments` object and renders
parallel calls newline-separated. Results become newline-delimited
`<tool_response>` blocks inside the synthetic user history message. The
scanner mints an id (`ptc_…`), emits `toolStart` as soon as the leading JSON
contains a complete string `name`, and waits for `</tool_call>` before emitting
`toolEnd`; it does not stream argument deltas. At close it uses the shared
scanner mints an id (`ptc_…`) and emits `toolStart` as soon as the leading JSON
contains a complete string `name`. It waits for `</tool_call>` before emitting
`toolEnd` and does not stream argument deltas. At close it uses the shared
repairing JSON parser. For compatibility it also accepts a stringified
`arguments` value and parses it once more, although the owned renderer never
emits that shape. A malformed completed block, a non-object argument value, or
an unfinished block does not become visible fallback prose: malformed calls
are consumed, with a string parse failure/non-object normalized to `{}` or the
whole call omitted when its outer object/name cannot be recovered.
emits that shape. A completed string parse failure or non-object argument
normalizes to `{}`; a completed outer object whose name cannot be recovered is
consumed without creating a call.
If EOF arrives after the name was recovered but before `</tool_call>`, no
`toolEnd` is emitted, but the canonical call created by `toolStart` survives
with empty arguments and may be dispatched on a normal stop. Malformed input
that never yields a name produces no call.
Thinking parsing is enabled by default: `<think>…</think>` becomes thinking
events and is excluded from visible text. Callers creating the scanner can set
@@ -235,7 +240,7 @@ text.
through vLLM's structured-outputs backend when using vLLM native tools, but
owned mode sends no native provider tool definition and therefore cannot rely
on that backend.
- **Version/scope:** this `hermes` template covers `Qwen3-*`, `Qwen2.5-*`, and `QwQ-32B`. It does **not** cover `Qwen3-Coder`, which uses a different XML scheme parsed by vLLM's `qwen3_xml` parser — a separate convention.
- **Version/scope:** this `hermes` template covers `Qwen3-*`, `Qwen2.5-*`, and `QwQ-32B`. It does **not** cover `Qwen3-Coder`, which uses a different XML scheme parsed by a serving engine's `qwen3_xml` parser. OMP has no `qwen3_xml` owned dialect; use `tools.format=native` and configure that parser at the endpoint.
## Sources
+241
View File
@@ -0,0 +1,241 @@
# Generic XML owned tool-calling format (`<invoke>` / `<tool_response>`)
OMP's `xml` dialect is a generic, prompt-driven in-band protocol. The model writes one `<invoke>` element per tool call directly in assistant text; OMP parses those calls and returns one ordered `<tool_response>` block per result in the next user turn. Neither side carries tool-call ids, and result blocks do not carry tool names, so ordering is the correlation mechanism.
This reference describes the converter implemented by `packages/ai/src/dialect/xml.ts`. The ordinary `tools.format: xml` path uses the shared Anthropic-style invoke scanner. The exported scanner API can instead select DeepSeek's pipe-wrapped DSML tagset; that scanner-only option is documented separately below.
## Selection and request conversion
Select the dialect in `~/.omp/agent/config.yml`, project config, or an overlay:
```yaml
tools:
format: xml
```
`tools.format: xml` forces the generic XML owned dialect for the session. `auto` does **not** choose generic XML as its unknown-family fallback: when a model has `supportsTools: false`, the resolver chooses the known model-family dialect or GLM if there is no specific affinity. Use `xml` explicitly when this grammar is required. See [`tools.format`](../settings.md#tools-and-approvals).
When selected, OMP removes native structured tools from the provider request, appends the in-band tool catalog and XML guide to the system prompt, converts prior structured calls/results to text, and scans assistant text back into structured tool-call events.
## Tool definitions and prompt injection
OMP injects the shared `# Tools` prompt. Available functions appear inside `<tools></tools>` as one compact OpenAI-style function object per line, using each tool's normalized wire schema:
```text
<tools>
{"type":"function","function":{"name":"read","description":"Read a file","parameters":{"type":"object","properties":{"path":{"type":"string"},"count":{"type":"number"}},"required":["path"]}}}
</tools>
```
The XML-specific guide from `packages/ai/src/dialect/xml.md` follows the catalog. It requires listed function names, literal string bodies, JSON non-string values, ordered results, and complete calls before the model stops. Calls are text, never native `tool_calls` JSON.
## Canonical call format
One call is one invoke:
```text
<invoke name="read"><parameter name="path">src/main.ts</parameter><parameter name="count">40</parameter></invoke>
```
| Element | Meaning |
| --- | --- |
| `<invoke name="TOOL">…</invoke>` | One tool call. The prompt contract requires a listed tool name. |
| `<parameter name="ARG">VALUE</parameter>` | One named argument. |
| `<tool_calls>…</tool_calls>` | Optional model-emitted wrapper accepted by the guide/scanner; OMP's renderer does not add it. |
`renderAssistantToolCalls` emits consecutive invokes separated by newlines, with no outer wrapper. The default scanner also accepts `<function_calls>` as a wrapper alias, `antml:`-prefixed variants of the Anthropic tags, and a bare invoke. Its accepted input is deliberately wider than the canonical renderer output.
Tool and parameter names are XML-escaped when OMP renders attributes. Parameter bodies are not XML-escaped because the format is delimiter-matched, not parsed by an XML DOM. Write `a & b < c`, not `a &amp; b &lt; c`; only a literal `</parameter>` conflicts with the body's close delimiter.
## Argument encoding and coercion
The renderer uses the supplied tool schema to decide whether a value is a literal string:
| Declared/value kind | Rendered body | Default scanner result |
| --- | --- | --- |
| Schema-declared string whose runtime value is a string | Verbatim, whitespace preserved | Verbatim string |
| Number, boolean, `null`, array, or object | JSON | Parsed JSON value |
| Runtime string not identified as a string argument | JSON string, including quotes | Parsed string |
Example:
```text
<invoke name="write"><parameter name="path">notes/a & b.txt</parameter><parameter name="options">{"append":false,"tags":["draft","xml"]}</parameter></invoke>
```
The default scanner accepts a `string` override on each parameter:
- `string="true"` (or any value other than `false`, `0`, or `no`) forces the raw body to remain a string.
- `string="false"`, `string="0"`, or `string="no"` forces JSON parsing even when the schema declares a string.
Non-string bodies are trimmed for parsing and passed through OMP's repair-capable JSON parser. If repair fails, the original body is retained as a string. Empty bodies remain empty strings. A parameter without a usable name is discarded.
## Multiple and parallel calls
OMP renders a batch as consecutive invokes:
```text
<invoke name="read"><parameter name="path">src/a.ts</parameter></invoke>
<invoke name="read"><parameter name="path">src/b.ts</parameter></invoke>
```
The model may optionally wrap the batch:
```text
<tool_calls>
<invoke name="read"><parameter name="path">src/a.ts</parameter></invoke>
<invoke name="read"><parameter name="path">src/b.ts</parameter></invoke>
</tool_calls>
```
The scanner mints one internal call id per invoke; there is no id in the XML. OMP can dispatch the calls as a batch. Results must preserve call order because `<tool_response>` has neither id nor name.
## Tool-result format
OMP returns each result in its own block:
```text
<tool_response>
file contents
</tool_response>
<tool_response>
ENOENT: file not found
</tool_response>
```
Consecutive result blocks are newline-separated and placed in one synthesized `user` message. Result text is inserted verbatim. Image blocks from tool results are retained after the rendered text in that message.
The generic XML protocol has **no success/error marker**. `renderToolResults` intentionally renders `isError: true` in the same `<tool_response>` shape as success; the error must be intelligible from its text. The model must never generate `<tool_response>` itself.
## Thinking and visible text
OMP renders preserved thinking as:
```text
<thinking>
reasoning text
</thinking>
```
For the normal owned-tool stream, `parseThinking` is enabled. With the default Anthropic tagset, `<thinking>`, `<think>`, and `<scratchpad>` (including supported prefixed forms) become separate thinking events and do not appear in visible text. A direct scanner consumer that leaves `parseThinking` false sees those tags as text. An unterminated thinking block is logically closed on flush and retains its content.
Visible prose may appear before or between unwrapped invokes. Inside a recognized `<tool_calls>` or `<function_calls>` wrapper, non-call text is discarded.
## Scanner tagsets
`XmlInbandScanner` delegates to one of two scanners according to `InbandScannerOptions.xmlTagset`:
| `xmlTagset` | Scanner | Accepted call grammar | Argument rule |
| --- | --- | --- | --- |
| omitted or `anthropic` | `AnthropicInbandScanner` | Plain/`antml:` `<invoke>/<parameter>`, optionally inside `<tool_calls>` or `<function_calls>` | Tool schema determines strings; `string` attribute can override |
| `dsml` | `DeepSeekInbandScanner` | Pipe-wrapped DSML envelope and invokes (plus that scanner's DeepSeek token grammar) | Parameters default to strings; only `string="false"` requests JSON coercion |
A direct API consumer can request DSML parsing:
```ts
import { createInbandScanner } from "@oh-my-pi/pi-ai/dialect";
const scanner = createInbandScanner("xml", {
xmlTagset: "dsml",
parseThinking: true,
});
```
DSML accepts fullwidth-pipe tags:
```text
<|DSML|tool_calls>
<|DSML|invoke name="read">
<|DSML|parameter name="path" string="true">src/a.ts</|DSML|parameter>
<|DSML|parameter name="count" string="false">2</|DSML|parameter>
</|DSML|invoke>
</|DSML|tool_calls>
```
It also accepts ASCII-pipe equivalents such as `<|DSML|tool_calls>`. In DSML mode, `string="false"` parses repaired JSON; invalid JSON falls back to the raw string. DSML thinking uses `<think>…</think>` and is parsed by default unless `parseThinking: false`.
`xmlTagset` changes **only scanner selection**. The `xml` definition's call, result, thinking, and transcript renderers always emit the generic plain-XML forms described above. The normal `tools.format: xml` owned-stream path does not pass `xmlTagset`, so it uses the Anthropic tagset. OMP currently uses the DSML selector for stream-markup healing of leaked DSML output, not to change the `tools.format: xml` renderer.
## Streaming, malformed output, and recovery
### Default Anthropic tagset
Parsing is incremental and safe across provider chunk boundaries. For every non-empty `<invoke name="…">`, the scanner:
1. emits `toolStart` as soon as the opening invoke tag is complete;
2. emits keyed `toolArgDelta` events while parameter bodies stream; and
3. performs final coercion and emits `toolEnd` only after the matching `</invoke>`.
The completed event includes the exact raw invoke block for diagnostics. Wrapper text is not part of that raw block.
Failure behavior is explicit:
- an invoke with a missing/blank name emits no tool lifecycle;
- a parameter with a missing/blank name is ignored;
- malformed JSON falls back to the original text;
- parameter content is capped at 1,000,000 JavaScript string code units, with an explicit truncation marker appended on overflow;
- an incomplete parameter or invoke emits no `toolEnd` when flushed; and
- complete invokes remain valid even when the outer wrapper never closes.
OMP's stream projector creates a canonical call at `toolStart`, before `toolEnd`. Therefore, on a normally stopped provider response, an unterminated invoke can remain as a partial runnable call: streamed argument text stays uncoerced, or arguments are `{}` if none arrived. A provider `length` stop remains non-runnable `length`. This behavior applies to the ordinary owned `xml` path and is important when diagnosing model output that stops mid-tag.
### DSML tagset
The DSML scanner also streams each parameter as keyed deltas and emits `toolEnd` only at `</|DSML|invoke>` or its ASCII equivalent. An incomplete DSML parameter resets the partial call on flush without a completed event. Because `xmlTagset: dsml` is a direct scanner option rather than the normal owned-renderer path, callers consuming those events own the handling of an unmatched `toolStart`.
### Fabricated results
For the generic XML dialect, the first model-authored `<tool_response>` is treated as a fabricated-result boundary. OMP preserves calls/text before it and stops projection there. The default `tools.abortOnFabricatedResult: true` aborts provider generation; disabling the setting drains but discards the fabricated continuation.
## End-to-end example
Injected catalog line:
```text
<tools>
{"type":"function","function":{"name":"get_weather","description":"Get weather","parameters":{"type":"object","properties":{"city":{"type":"string"},"days":{"type":"number"}},"required":["city"]}}}
</tools>
```
Assistant call batch:
```text
I'll compare both cities.
<invoke name="get_weather"><parameter name="city">Tokyo</parameter><parameter name="days">2</parameter></invoke>
<invoke name="get_weather"><parameter name="city">Oslo</parameter><parameter name="days">2</parameter></invoke>
```
Next user turn produced by OMP:
```text
<tool_response>
{"forecast":["clear","rain"]}
</tool_response>
<tool_response>
{"forecast":["rain","cloudy"]}
</tool_response>
```
The assistant then answers normally or emits another sequence of invokes.
## Parsing notes and gotchas
- **Not real XML.** Parameter bodies are delimiter-matched and intentionally unescaped. An XML parser/entity decoder changes their values.
- **Renderer and scanner acceptance differ.** OMP renders bare consecutive invokes; the default scanner additionally accepts two wrappers and `antml:` variants.
- **No call ids or result names.** Preserve call/result order across a parallel batch.
- **Errors are text only.** Generic `<tool_response>` does not encode `isError`.
- **Schema context matters.** Supply tools to renderer/scanner APIs so schema-declared strings remain literal rather than JSON-quoted/coerced.
- **`xmlTagset` is scanner-only.** Selecting DSML does not make the XML renderer emit DSML.
- **A close tag finalizes the call.** `toolStart` and argument deltas stream early, but only `</invoke>` produces the final coerced argument object and `toolEnd`.
## Sources
- `packages/ai/src/dialect/xml.md` — injected generic XML format guide.
- `packages/ai/src/dialect/xml.ts` — renderer definitions and Anthropic/DSML scanner selection.
- `packages/ai/src/dialect/anthropic.ts` — default incremental invoke/parameter scanner, coercion, thinking, and incomplete-call behavior.
- `packages/ai/src/dialect/deepseek.ts` — DSML envelope scanner and `string="false"` coercion.
- `packages/ai/src/dialect/catalog.ts` and `prompt-template.md` — tool catalog and system-prompt injection.
- `packages/ai/src/dialect/rendering.ts`, `history.ts`, and `owned-stream.ts` — result rendering, history conversion, projection, and fabricated-result handling.
- `packages/ai/src/utils/stream-markup-healing.ts` — current DSML scanner integration.
- `packages/coding-agent/src/sdk.ts` — `tools.format` resolution.
- `packages/ai/test/inband-tools.test.ts` and `dialect-thinking.test.ts` — round trips, chunked argument deltas, raw blocks, result rendering, and thinking behavior.