@caupulican/pi-adaptative 0.81.9 → 0.81.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/bundled-resources/skills/tool-call-repair/SKILL.md +148 -0
- package/dist/bundled-resources/skills/tool-call-repair/references/failure-grammar.md +128 -0
- package/dist/bundled-resources/skills/tool-call-repair/references/repair-catalogue.md +201 -0
- package/dist/bundled-resources/skills/tool-call-repair/references/text-protocol-grammar.md +141 -0
- package/dist/core/agent-session.d.ts +18 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +198 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/delegation/worker-actions.d.ts +2 -1
- package/dist/core/delegation/worker-actions.d.ts.map +1 -1
- package/dist/core/delegation/worker-actions.js +34 -2
- package/dist/core/delegation/worker-actions.js.map +1 -1
- package/dist/core/extensions/types.d.ts +3 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/failure-corpus.d.ts +27 -1
- package/dist/core/failure-corpus.d.ts.map +1 -1
- package/dist/core/failure-corpus.js +33 -11
- package/dist/core/failure-corpus.js.map +1 -1
- package/dist/core/model-registry.d.ts +2 -2
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +6 -4
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/models/adaptation-store.d.ts +62 -0
- package/dist/core/models/adaptation-store.d.ts.map +1 -0
- package/dist/core/models/adaptation-store.js +168 -0
- package/dist/core/models/adaptation-store.js.map +1 -0
- package/dist/core/session-analytics.d.ts +24 -1
- package/dist/core/session-analytics.d.ts.map +1 -1
- package/dist/core/session-analytics.js +75 -0
- package/dist/core/session-analytics.js.map +1 -1
- package/dist/core/settings-manager.d.ts +6 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +5 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt-builder.d.ts +4 -0
- package/dist/core/system-prompt-builder.d.ts.map +1 -1
- package/dist/core/system-prompt-builder.js +8 -0
- package/dist/core/system-prompt-builder.js.map +1 -1
- package/dist/core/tool-repair-health.d.ts +3 -0
- package/dist/core/tool-repair-health.d.ts.map +1 -0
- package/dist/core/tool-repair-health.js +43 -0
- package/dist/core/tool-repair-health.js.map +1 -0
- package/dist/core/tool-repair-settings.d.ts +11 -0
- package/dist/core/tool-repair-settings.d.ts.map +1 -0
- package/dist/core/tool-repair-settings.js +18 -0
- package/dist/core/tool-repair-settings.js.map +1 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +0 -9
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +10 -7
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +15 -9
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +7 -3
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +21 -4
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts.map +1 -1
- package/dist/modes/interactive/external-editor.js +5 -3
- package/dist/modes/interactive/external-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +37 -7
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +8 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +24 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/index.md +1 -0
- package/docs/rpc.md +36 -0
- package/docs/settings.md +20 -0
- package/docs/tool-repair.md +75 -0
- package/docs/usage.md +2 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +6 -4
- package/docs/bug-ledger.md +0 -134
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.81.12] - 2026-07-07
|
|
4
|
+
|
|
5
|
+
0.81.10/0.81.11 retracted before distribution; changes are folded into this section for 0.81.12.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Fixed external editor resolution so both inline and path-edit flows prefer `$VISUAL` before `$EDITOR`.
|
|
9
|
+
- Fixed registered extension models without token limits to receive default context and max-output limits.
|
|
10
|
+
- Fixed grep/find result-limit notices so exact-limit result sets are not reported as truncated.
|
|
11
|
+
- Fixed edit tool argument preparation to leave stringified `edits` for the shared validation repair layer.
|
|
12
|
+
- Added session analytics aggregation for shape-only tool argument validation telemetry.
|
|
13
|
+
- Added a host-keyed per-model adaptation store for repair rules, protocol calibration, and teach statistics.
|
|
14
|
+
- Added learned per-model tool-shape rules to system prompt assembly after repeated repair telemetry.
|
|
15
|
+
- Added per-model teach-efficacy aggregation for tool argument validation telemetry.
|
|
16
|
+
- Added per-model text tool-call protocol calibration before the first real turn.
|
|
17
|
+
- Added sanitized tool-validation bounce records to the local failure corpus for repair-mode discovery.
|
|
18
|
+
- Added tool repair health reporting, learned-rule removal, visible repaired-call markers, and repair/teach/protocol kill switches.
|
|
19
|
+
- Fixed delegated worker action parsing to use the shared tool-argument validation repair path.
|
|
20
|
+
- Documented tool repair diagnostics, controls, and replay workflow.
|
|
21
|
+
|
|
1
22
|
## [0.81.9] - 2026-07-07
|
|
2
23
|
|
|
3
24
|
## [0.81.8] - 2026-07-07
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tool-call-repair
|
|
3
|
+
description: Use when improving Pi's tool calling, when a model "can't do tool calls" or fails argument validation repeatedly, when adding an argument repair or coercion, or when touching validateToolArguments, prepareArguments, or provider tool-call assembly. Doctrine for making tool calling succeed with any model, especially open models with weak native tool support - validate-then-repair at one choke point, a finite failure-mode catalogue, shape-vs-relational rules, efficiency and telemetry requirements.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tool-call repair
|
|
7
|
+
|
|
8
|
+
A model that "can't do tool calls" is almost always emitting one of a SMALL,
|
|
9
|
+
FINITE set of shape mistakes (about 90% of cases across deepseek-flash,
|
|
10
|
+
deepseek v4 pro, glm, qwen; observed 2026-07). The harness's job is a
|
|
11
|
+
four-stage loop: **CAPTURE** every malformed or failing call, **FIX** what is
|
|
12
|
+
mechanically repairable, **RECOVER** the turn when repair is impossible
|
|
13
|
+
(bounce with a schema echo, never a dead end), and **TEACH** the model so the
|
|
14
|
+
mistake stops recurring. Every mechanical repair saves one full turn of
|
|
15
|
+
latency and tokens; every successful teach makes the repair unnecessary. That
|
|
16
|
+
compounding saving is the feature.
|
|
17
|
+
|
|
18
|
+
## Decisions (non-negotiable)
|
|
19
|
+
|
|
20
|
+
1. **One choke point, and it is CODE.** All argument repair lives behind
|
|
21
|
+
`validateToolArguments` (`packages/ai/src/utils/validation.ts`), which
|
|
22
|
+
every tool call already passes through (`packages/agent/src/agent-loop.ts`
|
|
23
|
+
prepareToolCall). The layer itself is a dedicated module of pure
|
|
24
|
+
deterministic functions - `utils/tool-repair/`: `registry.ts` (named
|
|
25
|
+
entries), `analyzer.ts` (validator errors -> classified modes),
|
|
26
|
+
`repairer.ts` (clone -> transform -> guard -> re-check),
|
|
27
|
+
`text-protocol.ts` (primer generator + envelope parser over one shared
|
|
28
|
+
grammar). No I/O, no clock, no randomness, no model calls anywhere in the
|
|
29
|
+
repair path; same input, same output; unit tests need no mocks. This
|
|
30
|
+
skill is doctrine for whoever maintains that module - prose never
|
|
31
|
+
substitutes for the parser/analyzer/repairer code. Never add per-tool
|
|
32
|
+
special cases; `prepareEditArguments` in `edit.ts` is legacy exactly-this
|
|
33
|
+
and gets deleted when the general repair covers it.
|
|
34
|
+
2. **Validate, then repair.** Run `Check` first. Well-formed args are NEVER
|
|
35
|
+
mutated (hot path pays one cached compiled-validator check, returns the
|
|
36
|
+
SAME object, nothing else). On failure, walk the validator's error list
|
|
37
|
+
and apply repairs keyed to the error type at the failing path, then
|
|
38
|
+
re-Check once. Still failing: bounce to the model as an error tool result
|
|
39
|
+
that echoes the expected schema fragment. Never preprocess-then-validate;
|
|
40
|
+
blind coercion mutates correct input and hides bugs.
|
|
41
|
+
2a. **Performance is a hard requirement, not a hope.** Completeness lives
|
|
42
|
+
ENTIRELY on the slow (already-failed) path; the fast path is one cached
|
|
43
|
+
`Check`. Repairs are O(validation-errors), not O(schema): a static
|
|
44
|
+
`(expect, got)` dispatch Map, precompiled matchers, no RegExp/schema
|
|
45
|
+
compilation/JSON parse per call except the one a mode explicitly needs.
|
|
46
|
+
Bounded work: ≤1 clone, ≤1 transform per failing path, ≤1 sub-Check per
|
|
47
|
+
transform, exactly 1 whole-args re-Check, no transform loops. A microbench
|
|
48
|
+
fixture GATES this (clean-path cost within noise of a bare Check). Adding
|
|
49
|
+
the 20th failure mode must cost the hot path nothing.
|
|
50
|
+
3. **Shape repairs only.** A repair may reshape what the model clearly meant
|
|
51
|
+
(parse a stringified array, wrap a bare item, drop a null optional). It may
|
|
52
|
+
never invent a value. Required-but-null, cross-field constraints, and
|
|
53
|
+
semantic errors are relational: bounce those to the model. The current
|
|
54
|
+
`coercePrimitiveByType` null-to-zero-value behavior violates this rule and
|
|
55
|
+
is scheduled to be replaced by the catalogue's null rule.
|
|
56
|
+
4. **Uniform across schema kinds.** TypeBox tools (built-ins) and plain
|
|
57
|
+
JSON-schema tools (MCP, extensions) get the same repair set. Today
|
|
58
|
+
built-ins get FEWER repairs (the `hasTypeBoxMetadata` guard skips the
|
|
59
|
+
coercion pass); that asymmetry is a defect, not a design.
|
|
60
|
+
5. **Every repair is check-guarded and ordered.** Apply on a cloned candidate,
|
|
61
|
+
keep only if the sub-check passes. Within string-where-array-expected, try
|
|
62
|
+
JSON.parse FIRST and wrap second, or `'["a","b"]'` becomes `['["a","b"]']`.
|
|
63
|
+
6. **Telemetry or it does not improve.** Count `{model, failureMode,
|
|
64
|
+
repaired|bounced|taught}` per firing, and link each repair to its
|
|
65
|
+
execution outcome and to recurrence-after-teach. That table tells us which
|
|
66
|
+
models need which repairs, proves the round-trips saved, and marks dead
|
|
67
|
+
repairs and dead teach notes for removal. A repair that never fires in 30
|
|
68
|
+
days is a deletion candidate.
|
|
69
|
+
7. **Teach, do not nag.** A silent repair leaves the model repeating the
|
|
70
|
+
mistake forever. When a repair fires, the tool result carries a ONE-LINE
|
|
71
|
+
corrective note ("your `edits` arrived as a JSON string; it was repaired
|
|
72
|
+
to an array - emit a raw JSON array") the FIRST time that (tool, mode)
|
|
73
|
+
fires in a session, throttled after. A mode that keeps firing for a model
|
|
74
|
+
graduates to a standing per-model rule injected into the system prompt,
|
|
75
|
+
capped and retired when telemetry shows it stopped firing. Teaching that
|
|
76
|
+
does not reduce recurrence gets reworded or dropped, measured, not
|
|
77
|
+
assumed.
|
|
78
|
+
8. **Only detectable failures can be taught.** A well-formed call that does
|
|
79
|
+
the wrong thing is invisible to this layer except for enumerated
|
|
80
|
+
detectable subclasses (see the teachable-error catalogue in references).
|
|
81
|
+
Do not pretend otherwise; general semantic wrongness belongs to review,
|
|
82
|
+
not to the repair loop.
|
|
83
|
+
|
|
84
|
+
## Two grammars (the complete contract)
|
|
85
|
+
|
|
86
|
+
The layer is defined by two formal grammars, both in references, both encoded
|
|
87
|
+
as code (not prose):
|
|
88
|
+
- **Envelope grammar** (`references/text-protocol-grammar.md`) — how a text-
|
|
89
|
+
mode call is shaped: EBNF for `<pi:call name="X">{json}</pi:call>`, the
|
|
90
|
+
recognized inbound variants, the schema→primer projection, and the
|
|
91
|
+
DICTIONARY generated from pi's real tools (bash, read, edit, write, ls,
|
|
92
|
+
grep, find).
|
|
93
|
+
- **Failure grammar** (`references/failure-grammar.md`) — how a malformed
|
|
94
|
+
call is recognized and repaired: the `errorSignature → transform → guard →
|
|
95
|
+
note` table (modes 1–10, tool-agnostic) plus the tool-specific rows for
|
|
96
|
+
bash (`command` as argv-array or object-wrapper, `timeout` as string/{}),
|
|
97
|
+
edit, read/ls/grep/find. This is what `analyzer.ts` classifies against.
|
|
98
|
+
|
|
99
|
+
## The catalogue (summary; full table in references/failure-grammar.md)
|
|
100
|
+
|
|
101
|
+
| # | Name | Model emits | Repair (guard-gated) |
|
|
102
|
+
|---|---|---|---|
|
|
103
|
+
| 1 | nullOptionalDrop | `null` for an optional field | delete key (required+null → bounce) |
|
|
104
|
+
| 2 | jsonStringParse | `"[...]"`/`"{...}"` string where container expected | JSON.parse; keep if it matches + checks |
|
|
105
|
+
| 3 | singleObjectWrap | single object where array-of-objects expected | wrap `[obj]` if it passes `items` |
|
|
106
|
+
| 4 | bareScalarWrap | bare scalar where array expected | wrap `[v]` if it passes `items` |
|
|
107
|
+
| 5 | emptyObjectPlaceholder | `{}` placeholder where scalar expected | delete if optional (default applies); else bounce |
|
|
108
|
+
| 6 | numberFromString | `"42"` where number expected | `Number(s)` if finite |
|
|
109
|
+
| 7 | boolFromString | `"true"`/`"false"` where bool expected | exact map (never truthiness) |
|
|
110
|
+
| 8 | enumCaseNormalize | case/space enum variant | match to the one member, else bounce |
|
|
111
|
+
| 9 | singleElementUnwrap | `[v]` where scalar expected | unwrap if 1 elem and checks |
|
|
112
|
+
| 10 | stringifiedNumberInArray | `["1","2"]` where number[] expected | map Number if all finite |
|
|
113
|
+
|
|
114
|
+
Plus bash-specific: `bashCommandArgvJoin` (argv list → joined string) and
|
|
115
|
+
`bashCommandUnwrap` (`{cmd}` → string). Every entry is a NAMED registry entry
|
|
116
|
+
`{name, errorSignature, transform, guard, noteTemplate}` — one table powers
|
|
117
|
+
the repair, the teach note, the telemetry tag, and the docs row: the
|
|
118
|
+
deterministic set of what pi can and will repair. Repairs NEVER invent values
|
|
119
|
+
and are ALWAYS guard-gated (kept only if the transform Checks), so a repair
|
|
120
|
+
can only ever turn an invalid call valid, never alter a valid one. Modes 6–10
|
|
121
|
+
and the bash rows are the increment past the original four; they cost the hot
|
|
122
|
+
path nothing because they run only on already-failed calls (see decision 2a).
|
|
123
|
+
|
|
124
|
+
## Method for an improvement pass
|
|
125
|
+
|
|
126
|
+
Measure first (telemetry, or fixture-replay of failing transcripts). Pick the
|
|
127
|
+
top firing failure mode. Write a failing fixture per affected provider. Land
|
|
128
|
+
the error-keyed repair at the choke point. Prove the hot path is unchanged
|
|
129
|
+
(well-formed fixture bytes untouched). Record ledger + CHANGELOG entries per
|
|
130
|
+
repo convention. Adjacent gaps (missing tool-call id synthesis, text tool-call
|
|
131
|
+
extraction and the plain-text protocol primer + calibration handshake for
|
|
132
|
+
untrained models, truncated-stream guard, name sanitization parity,
|
|
133
|
+
history-replay policy) are catalogued with code anchors in the references
|
|
134
|
+
file; take them as separate items, not riders.
|
|
135
|
+
|
|
136
|
+
## Output contract
|
|
137
|
+
|
|
138
|
+
Code and fixtures land in `packages/ai` (validation + provider tests).
|
|
139
|
+
Per-model observations and new failure modes append to the model-notes table
|
|
140
|
+
in `references/repair-catalogue.md`. Durable findings go to the AGENTS.md
|
|
141
|
+
ledger. Never chat-only.
|
|
142
|
+
|
|
143
|
+
## Guards
|
|
144
|
+
|
|
145
|
+
- Never loosen a schema or delete a test to make a call pass.
|
|
146
|
+
- Repairs are pure, deterministic, bounded (~100 lines each, one re-Check).
|
|
147
|
+
- A repair without a fixture per provider it claims to fix does not merge.
|
|
148
|
+
- Do not repair relational failures; the model must decide values.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Failure grammar — the deterministic error→repair map
|
|
2
|
+
|
|
3
|
+
Two grammars make the layer complete: the ENVELOPE grammar
|
|
4
|
+
(`text-protocol-grammar.md`) says how a call is shaped; this FAILURE grammar
|
|
5
|
+
says how a malformed call is recognized and repaired. It is the formal
|
|
6
|
+
contract `analyzer.ts` classifies against and `registry.ts` encodes. Every
|
|
7
|
+
entry is `errorSignature → transform → guard → note`, keyed to a validator
|
|
8
|
+
error at an instance path. Adding a mode = adding a row here + a registry
|
|
9
|
+
entry + fixtures. Nothing is repaired that is not in this table.
|
|
10
|
+
|
|
11
|
+
Notation: `P` = failing instance path; `expect(P)` = the JSON-schema type(s)
|
|
12
|
+
declared at P; `got` = the received value's runtime type; `req(P)` = whether P
|
|
13
|
+
is in its parent's `required`; `def(P)` = P has a schema `default`.
|
|
14
|
+
|
|
15
|
+
## Core failure grammar (tool-agnostic — applies to every schema)
|
|
16
|
+
|
|
17
|
+
| # | name | errorSignature | got | transform | guard (keep iff) | note |
|
|
18
|
+
|---|---|---|---|---|---|---|
|
|
19
|
+
| 1 | nullOptionalDrop | type mismatch at P | `null`, `!req(P)` | delete key P | always (absence is valid for optional) | "sent null for optional `P` → omit the field instead" |
|
|
20
|
+
| 1b | nullRequiredBounce | type mismatch at P | `null`, `req(P)`, `!def(P)` | none | never (bounce) | "`P` is required and cannot be null → send a real value" |
|
|
21
|
+
| 2 | jsonStringParse | expect array\|object at P, got string | `"[...]"`/`"{...}"` | `JSON.parse(s)` | parsed matches expect(P) AND sub-checks | "sent `P` as a quoted JSON string → send a raw JSON array/object" |
|
|
22
|
+
| 3 | singleObjectWrap | expect array at P, got object | `{...}` | `[obj]` | `[obj]` passes `items(P)` | "sent one object where `P` takes a list → wrap it in `[ ]`" |
|
|
23
|
+
| 4 | bareScalarWrap | expect array at P, got scalar | string/number/bool | `[v]` | `[v]` passes `items(P)` | "sent a single value where `P` takes a list → wrap it in `[ ]`" |
|
|
24
|
+
| 5 | emptyObjectPlaceholder | expect scalar at P (or array whose `items` reject `{}`), got object | `{}` | delete key P | `!req(P)` (schema default applies) else bounce | "sent `{}` as a placeholder → omit `P`; its default applies" |
|
|
25
|
+
| 6 | numberFromString | expect number/integer at P, got string | `"42"`, numeric | `Number(s)` | finite (and integer if integer(P)) | "sent `P` as a quoted number → send a bare number" |
|
|
26
|
+
| 7 | boolFromString | expect boolean at P, got string | `"true"`/`"false"` | `s === "true"` | exact match only (never truthiness) | "sent `P` as a quoted boolean → send bare true/false" |
|
|
27
|
+
| 8 | enumCaseNormalize | expect enum at P, got string not in set | case/space variant | match case-insensitively/trimmed to one enum member | exactly one member matches | "`P` must be one of `a|b|c` → matched `<value>`" |
|
|
28
|
+
| 9 | singleElementUnwrap | expect scalar at P, got 1-elem array | `[v]` | `v` | `v` passes expect(P) AND `length===1` | "sent `P` as a 1-item list where a single value was expected → send the value" |
|
|
29
|
+
| 10 | stringifiedNumberInArray | expect number[] at P, got string[] | `["1","2"]` | map `Number` | all finite | "list `P` holds quoted numbers → send bare numbers" |
|
|
30
|
+
|
|
31
|
+
Rules that keep this deterministic and safe:
|
|
32
|
+
- **Repairs never invent a value.** 5 relies on the schema `default`; nothing
|
|
33
|
+
fabricates content. If a guard fails, the mode does not apply → bounce.
|
|
34
|
+
- **Guard is mandatory.** Every transform runs on a clone and is kept ONLY if
|
|
35
|
+
it Checks against the sub-schema at P. A transform whose guard fails leaves
|
|
36
|
+
args untouched and falls through to the next applicable mode, then bounce.
|
|
37
|
+
- **Order within one path:** 2 (parse) → 8/6/7 (coerce scalar) → 3/4/10 (wrap)
|
|
38
|
+
→ 9 (unwrap) → 5 (placeholder-drop) → 1 (null-drop). Parse before wrap so a
|
|
39
|
+
stringified array becomes an array, not a wrapped string. Across paths:
|
|
40
|
+
instance-path order.
|
|
41
|
+
- **One re-Check.** After all per-path transforms, Check the whole args once.
|
|
42
|
+
Pass → return repaired; fail → bounce. Never loop transforms.
|
|
43
|
+
- Modes 6–10 are the increment past the original four; each is guard-gated so
|
|
44
|
+
it can only ever turn an invalid call into a valid one, never change a
|
|
45
|
+
valid call (the hot path never reaches them — see Performance).
|
|
46
|
+
|
|
47
|
+
## Tool-specific failure grammar (bash and the shape-sensitive tools)
|
|
48
|
+
|
|
49
|
+
The core grammar is schema-driven and covers every tool automatically. A few
|
|
50
|
+
tools have a KNOWN, recurring, tool-shaped failure that the core grammar sees
|
|
51
|
+
only as a generic type error; naming them lets the note teach precisely.
|
|
52
|
+
These are still guard-gated core repairs — NOT special-cased logic — the
|
|
53
|
+
tool-specificity lives only in the note text and the telemetry tag.
|
|
54
|
+
|
|
55
|
+
### bash(command:string, timeout:number?)
|
|
56
|
+
`command` is the arg models most often mis-shape, and bash is the highest-
|
|
57
|
+
traffic tool, so its failures matter most.
|
|
58
|
+
|
|
59
|
+
| observed | core mode | note (bash-specific) |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `command` sent as `["ls","-la"]` (argv array) | bareScalarWrap inverse → join | "bash takes ONE command string, not an argv list → joined to `ls -la`" (guard: every element string; join with spaces) |
|
|
62
|
+
| `command: {"cmd":"ls"}` (object wrapper) | emptyObjectPlaceholder sibling: unwrap known single key | "bash `command` is a string → unwrapped `{cmd}`" (guard: single string-valued key in {cmd,command,script}) |
|
|
63
|
+
| `timeout: "30"` | numberFromString | "timeout is a number of seconds → sent 30" |
|
|
64
|
+
| `timeout: {}` | emptyObjectPlaceholder | "sent `{}` for timeout → omitted; silence watchdog applies" |
|
|
65
|
+
| `timeout: 0` / negative | NOT a failure — schema-valid, tool treats as unset | (no repair; documented tool behavior) |
|
|
66
|
+
|
|
67
|
+
The argv-array and object-wrapper `command` cases are common enough to be
|
|
68
|
+
named registry entries (`bashCommandArgvJoin`, `bashCommandUnwrap`) with the
|
|
69
|
+
same guard-then-keep discipline; they only fire when the core string check
|
|
70
|
+
already failed.
|
|
71
|
+
|
|
72
|
+
### edit(path, edits:{oldText,newText}[])
|
|
73
|
+
The canonical array-shape offender (Opus 4.6, GLM-5.1 in the code comment).
|
|
74
|
+
|
|
75
|
+
| observed | core mode | note |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `edits: "[{...}]"` | jsonStringParse | "sent `edits` as a quoted JSON string → send a raw array" |
|
|
78
|
+
| `edits: {oldText,newText}` | singleObjectWrap | "sent one edit object → wrap in `[ ]`" |
|
|
79
|
+
| `oldText`/`newText` at top level (no `edits`) | legacy adapter (NOT a repair) | migrated to `edits:[{...}]` by the existing edit adapter |
|
|
80
|
+
|
|
81
|
+
### read / ls / grep / find (numeric + enum args)
|
|
82
|
+
| observed | core mode | note |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `offset`/`limit`/`context`/`tail`: `"10"` | numberFromString | "`P` is a number → sent 10" |
|
|
85
|
+
| `limit`/`offset`: `null` | nullOptionalDrop | "omit `P` to use the default" |
|
|
86
|
+
| read `filter`: `"None"`/`" minimal"` | enumCaseNormalize | "`filter` is one of none/minimal/aggressive → matched `<value>`" |
|
|
87
|
+
| `ignoreCase`/`literal`/`metadata`: `"true"` | boolFromString | "`P` is a boolean → sent true" |
|
|
88
|
+
|
|
89
|
+
## What is deliberately NOT in the grammar (bounce, do not repair)
|
|
90
|
+
|
|
91
|
+
- Missing REQUIRED args with no default — the model must supply intent.
|
|
92
|
+
- Wrong VALUE that is validly typed (e.g. a nonexistent path, a bad regex) —
|
|
93
|
+
not a shape error; belongs to execution + R44 teachable-errors, not repair.
|
|
94
|
+
- Ambiguous enum (matches 2+ members) — bounce with the candidate list.
|
|
95
|
+
- Any transform whose guard fails — never force it.
|
|
96
|
+
- Semantic wrongness in a well-formed call — out of mechanical reach.
|
|
97
|
+
|
|
98
|
+
## Performance contract (binding — the grammar must not slow the harness)
|
|
99
|
+
|
|
100
|
+
The failure grammar only ever runs on ALREADY-FAILED calls. The hot path
|
|
101
|
+
(well-formed args) never touches it. Concretely:
|
|
102
|
+
|
|
103
|
+
1. **Check-first, allocation-free on success.** A valid call pays exactly one
|
|
104
|
+
`validator.Check(args)` (cached compiled validator) and returns the SAME
|
|
105
|
+
object — no clone, no walk, no grammar lookup. This is the ~99% path for
|
|
106
|
+
strong models and it is unchanged from today's cost.
|
|
107
|
+
2. **Repairs are O(errors), not O(schema).** The analyzer walks the
|
|
108
|
+
validator's error list (already produced by the failed Check), not the
|
|
109
|
+
schema tree. Each error maps to at most a few candidate modes by a static
|
|
110
|
+
dispatch table keyed on `(expect, got)` — a Map lookup, not a scan.
|
|
111
|
+
3. **No per-call compilation.** errorSignature matchers are precompiled
|
|
112
|
+
constants; note templates are format strings; the registry is built once
|
|
113
|
+
at module load. Nothing in the repair path constructs a RegExp, compiles a
|
|
114
|
+
schema, or parses JSON except the one `JSON.parse` a mode explicitly needs.
|
|
115
|
+
4. **Bounded work.** At most: one clone of args, one transform per failing
|
|
116
|
+
path, one sub-Check per attempted transform, one whole-args re-Check. No
|
|
117
|
+
loops over transforms, no backtracking search. Worst case is linear in the
|
|
118
|
+
number of validation errors, which is itself tiny.
|
|
119
|
+
5. **Text protocol only for flagged models.** The parser (R38) runs only when
|
|
120
|
+
a model is configured for text mode; native-tool-call models never invoke
|
|
121
|
+
it. The primer is generated once per session, not per turn.
|
|
122
|
+
6. **Measured.** A microbench fixture asserts the clean-path cost is within
|
|
123
|
+
noise of a bare `Check` (no regression), and that a repaired call stays
|
|
124
|
+
under a fixed budget. Perf is an acceptance criterion, not an aspiration.
|
|
125
|
+
|
|
126
|
+
The design principle: completeness lives entirely on the SLOW (already-broken)
|
|
127
|
+
path; the FAST path is a single cached check. Adding the 6th–20th failure mode
|
|
128
|
+
costs the hot path nothing.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Repair catalogue and harness audit map
|
|
2
|
+
|
|
3
|
+
Source: cross-model repair work on deepseek-flash, deepseek v4 pro, glm, qwen
|
|
4
|
+
(owner, 2026-07) plus the 2026-07-06 pi-adaptative harness audit. Line numbers
|
|
5
|
+
are anchors from that date; trust the named function over the number.
|
|
6
|
+
|
|
7
|
+
## The repairs, precisely (named registry)
|
|
8
|
+
|
|
9
|
+
Repairs run ONLY after a failed `Check`, keyed to the validator error at the
|
|
10
|
+
failing instance path. Each candidate is applied on a clone and kept only if
|
|
11
|
+
the sub-schema check passes. One re-Check of the whole args after all repairs;
|
|
12
|
+
still failing means bounce to the model.
|
|
13
|
+
|
|
14
|
+
Each repair is one registry entry: `{name, errorSignature, transform, guard,
|
|
15
|
+
noteTemplate}`. The registry is the deterministic contract of what pi can and
|
|
16
|
+
will repair; the same entry powers the repair, the teach note, the telemetry
|
|
17
|
+
tag, and the docs table. Adding a repair means adding one entry plus its
|
|
18
|
+
fixtures, nothing else.
|
|
19
|
+
|
|
20
|
+
### 1. null-for-optional (`nullOptionalDrop`)
|
|
21
|
+
- Error signature: type mismatch at path P, received `null`, P not in the
|
|
22
|
+
parent's `required` list.
|
|
23
|
+
- Repair: delete key P from its parent object.
|
|
24
|
+
- If P IS required: no repair. Bounce. Inventing `0`/`""`/`false` corrupts
|
|
25
|
+
intent (`limit: null` becoming `limit: 0` changes the program). This
|
|
26
|
+
replaces the current `coercePrimitiveByType` null branches.
|
|
27
|
+
|
|
28
|
+
### 2. json-string-encoded array or object (`jsonStringParse`)
|
|
29
|
+
- Error signature: expected array (or object) at P, received string.
|
|
30
|
+
- Repair: `JSON.parse` the string; keep iff the parsed value is the expected
|
|
31
|
+
container type AND passes the sub-schema check.
|
|
32
|
+
- MUST be attempted before repair 4 on the same error, or a stringified array
|
|
33
|
+
gets wrapped instead of parsed.
|
|
34
|
+
|
|
35
|
+
### 3. single object where array-of-objects expected (`singleObjectWrap`)
|
|
36
|
+
- Error signature: expected array at P, received object.
|
|
37
|
+
- Repair: wrap as `[obj]` iff `obj` passes the array's `items` schema.
|
|
38
|
+
- Covers the "empty placeholder" variant: `{}` where `items` has no required
|
|
39
|
+
fields wraps legitimately; `{}` where `items` requires fields fails the
|
|
40
|
+
guard and bounces (relational: the model owes the content).
|
|
41
|
+
|
|
42
|
+
### 4. bare scalar where array expected (`bareScalarWrap`)
|
|
43
|
+
- Error signature: expected array at P, received string/number/boolean.
|
|
44
|
+
- Repair: wrap as `[v]` iff `v` passes `items`. Runs after repair 2.
|
|
45
|
+
|
|
46
|
+
### 5. empty-object placeholder (`emptyObjectPlaceholder`)
|
|
47
|
+
- Error signature: expected scalar (number/integer/string/boolean) at P,
|
|
48
|
+
received `{}`; OR expected array at P, received `{}` that fails repair 3's
|
|
49
|
+
guard (its `items` requires fields `{}` lacks).
|
|
50
|
+
- Repair: treat `{}` as ABSENT. Optional P: delete the key — a declared
|
|
51
|
+
schema default then applies naturally (`shell({cmd, {} })` becomes
|
|
52
|
+
`shell({cmd})` and the default idleTime supplies the value). Required P
|
|
53
|
+
with a schema `default`: also deletable only if the validator honors the
|
|
54
|
+
default; otherwise bounce. Required P without default: bounce.
|
|
55
|
+
- The rule that keeps this deterministic: the repair never invents the
|
|
56
|
+
value; the schema's declared default supplies it, or the model must.
|
|
57
|
+
|
|
58
|
+
Ordering rule inside one error: parse (2) → wrap (3/4) → treat-as-absent (5).
|
|
59
|
+
Across errors: independent paths are order-free; process in instance-path
|
|
60
|
+
order for determinism.
|
|
61
|
+
|
|
62
|
+
## Layer architecture (deterministic code, not prompts)
|
|
63
|
+
|
|
64
|
+
Module: `packages/ai/src/utils/tool-repair/`
|
|
65
|
+
|
|
66
|
+
| File | Responsibility | Purity contract |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `registry.ts` | the named repair entries: `{name, errorSignature, transform, guard, noteTemplate}` as typed data + pure functions | no side effects; enumerable (docs + telemetry tags generate from it) |
|
|
69
|
+
| `analyzer.ts` | validator error list -> classified failure modes at instance paths | pure; same errors, same classification |
|
|
70
|
+
| `repairer.ts` | clone -> transform -> guard-check -> whole-args re-check orchestration | pure; never mutates input; no I/O/clock/randomness |
|
|
71
|
+
| `text-protocol.ts` | ONE grammar definition; primer generator (Tool[] -> primer string) and envelope parser (text -> toolCall blocks) over it | pure; no regex scattered in providers |
|
|
72
|
+
|
|
73
|
+
`validateToolArguments` is only the thin integration point (Check ->
|
|
74
|
+
analyzer -> repairer -> re-Check -> bounce). Nothing in the repair path
|
|
75
|
+
calls a model. Offline proving ground: `scripts/tool-repair-replay.mjs`
|
|
76
|
+
replays corpus records or session files through analyzer/repairer with no
|
|
77
|
+
LLM and no session, reporting classified | would-repair | would-bounce and
|
|
78
|
+
emitting fixtures - new modes are developed against the script before
|
|
79
|
+
touching the live path.
|
|
80
|
+
|
|
81
|
+
## Choke-point map (audited 2026-07-06)
|
|
82
|
+
|
|
83
|
+
| Layer | Anchor | State |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| Streaming args assembly | each provider, `parseStreamingJson(partialJson)`; e.g. `openai-completions.ts` ~:382, `anthropic.ts` ~:623 | tolerant partial parse; no repair here (correct; keep providers dumb) |
|
|
86
|
+
| Blind pre-repair hook | `Tool.prepareArguments`, only user: `edit.ts` `prepareEditArguments` ~:95 | legacy special case of repair 2 for `edits` only (comment names GLM-5.1); delete once general repair lands |
|
|
87
|
+
| Scalar conversion | `validation.ts` ~:294 `Value.Convert` | fine (string "5" to 5 etc.) |
|
|
88
|
+
| Custom coercion | `validation.ts` `coerceWithJsonSchema` ~:205 | runs ONLY for non-TypeBox schemas (`hasTypeBoxMetadata` guard ~:297): built-ins get fewer repairs, backwards. null-to-zero-value branches ~:80-145 violate the relational rule |
|
|
89
|
+
| Check + error | `validation.ts` ~:311-323 | per-path errors + received args; does NOT echo expected schema fragment |
|
|
90
|
+
| Failure feedback | `agent-loop.ts` ~:700-704 | validation error becomes error tool result; model-side retry is the only recovery today |
|
|
91
|
+
| Check-guarded pattern precedent | `validation.ts` `coerceWithUnionSchema` ~:193 | clone, coerce, Check; reuse this shape for all repairs |
|
|
92
|
+
|
|
93
|
+
## Adjacent gap backlog (separate items, code-anchored)
|
|
94
|
+
|
|
95
|
+
1. **Missing tool-call id synthesis.** `openai-completions.ts` ~:254 seeds
|
|
96
|
+
`id: toolCall.id || ""`. Open-model servers sometimes omit or duplicate
|
|
97
|
+
ids; an empty id breaks toolResult matching and replay. Synthesize a
|
|
98
|
+
stable id when absent (e.g. `call_<n>` per message) at assembly time.
|
|
99
|
+
2. **Text tool-call extraction.** No fallback for serving stacks that never
|
|
100
|
+
emit native `tool_calls` (intent arrives as `<tool_call>{...}</tool_call>`
|
|
101
|
+
or fenced JSON in content). Models in that population cannot tool-call
|
|
102
|
+
through pi at all; repairs never see them. Extractor would live at the
|
|
103
|
+
provider boundary, gated per model/config, off by default.
|
|
104
|
+
3. **Schema echo in validation errors.** The bounce message should include
|
|
105
|
+
the expected sub-schema fragment (and one valid example) at each failing
|
|
106
|
+
path; per-path error text alone often reproduces the same retry mistake.
|
|
107
|
+
4. **Truncated-stream guard.** `parseStreamingJson` happily yields a partial
|
|
108
|
+
object when the stream ends mid-args (finish_reason length). If the
|
|
109
|
+
partial validates (all lost fields optional), the tool executes with
|
|
110
|
+
silently missing args. Detect terminal-with-incomplete-json and bounce.
|
|
111
|
+
5. **Name sanitization parity.** `google-shared.ts` ~:95 sanitizes tool names
|
|
112
|
+
to `[a-zA-Z0-9_-]{1,64}`; the OpenAI-compat path sends names raw. MCP
|
|
113
|
+
tools with dots/colons 400 on strict backends.
|
|
114
|
+
6. **History replay policy (decision needed).** Execution uses repaired args
|
|
115
|
+
but the assistant message keeps the model's original emission, and
|
|
116
|
+
`openai-completions.ts` ~:896 replays that original with a success result.
|
|
117
|
+
The model is shown that the malformed shape worked, reinforcing it every
|
|
118
|
+
turn. Options: replay repaired args (transcript diverges from what the
|
|
119
|
+
model emitted) or keep original (reinforcement). Decide once, document,
|
|
120
|
+
apply across providers.
|
|
121
|
+
7. **Escalation counter.** N identical validation failures from the same
|
|
122
|
+
model on the same tool should trigger something better than an identical
|
|
123
|
+
retry: inject the schema example, or escalate the model tier for one turn
|
|
124
|
+
(see `model-router/tool-escalation.ts` for the existing escalation shape).
|
|
125
|
+
|
|
126
|
+
## Teach-back spec (capture -> fix -> recover -> teach)
|
|
127
|
+
|
|
128
|
+
The repair engine fixes the call; teach-back fixes the MODEL. Three tiers,
|
|
129
|
+
cheapest first:
|
|
130
|
+
|
|
131
|
+
1. **In-band note (per call).** When a repair fires, append one line to the
|
|
132
|
+
tool result: `[harness] <repairName>: <what was wrong> -> <the right
|
|
133
|
+
form>; executed as <repaired call>.` The IN -> OUT echo shows the model
|
|
134
|
+
exactly what its call became - the strongest teaching signal; the
|
|
135
|
+
repaired call and the repair note travel together in the same
|
|
136
|
+
tool-result message. Text comes from the registry entry's noteTemplate.
|
|
137
|
+
Fire on the FIRST (tool, repairName) occurrence per session; afterwards
|
|
138
|
+
every Nth occurrence (default 5). One line, no lecture; token cost is
|
|
139
|
+
the budget being protected.
|
|
140
|
+
2. **Standing per-model rule (persistent).** When telemetry shows a
|
|
141
|
+
(model, failureMode) pair firing above threshold (default: 3 in one
|
|
142
|
+
session, or recurring across sessions), inject a standing rule into that
|
|
143
|
+
model's system prompt via the existing system-prompt builder ("emit arrays
|
|
144
|
+
as JSON arrays, never as quoted strings"). Persist in the per-model
|
|
145
|
+
profile. Hard cap (default 5 rules per model); retire any rule after 30
|
|
146
|
+
days of telemetry silence. Rules are shape rules only, never tool-specific
|
|
147
|
+
prose walls.
|
|
148
|
+
3. **Teachable-error catalogue (execution level).** Common EXECUTION errors
|
|
149
|
+
get corrective guidance authored once, centrally: file-not-found suggests
|
|
150
|
+
listing the directory first; edit text-not-found suggests re-reading the
|
|
151
|
+
file; identical repeated failing calls escalate wording ("the same call
|
|
152
|
+
failed twice; change the arguments, do not resend"). Enumerated list, not
|
|
153
|
+
heuristics; each entry names its trigger error class.
|
|
154
|
+
|
|
155
|
+
Efficacy is measured or the note dies: recurrence of the same (model, mode)
|
|
156
|
+
after a teach event must drop; a note whose recurrence does not drop gets
|
|
157
|
+
reworded or removed. "Wrong but successful" calls are OUT of mechanical reach
|
|
158
|
+
except enumerated detectable subclasses (edit producing zero net change, read
|
|
159
|
+
of an empty range treated as content, and similar); anything else is review
|
|
160
|
+
territory, not repair territory.
|
|
161
|
+
|
|
162
|
+
## Telemetry spec
|
|
163
|
+
|
|
164
|
+
Emit one event per validation outcome: `{model, provider, tool, outcome:
|
|
165
|
+
clean|repaired|bounced, failureModes: [1..5|other], repairsApplied:
|
|
166
|
+
[registry names], taught: none|note|rule}`. Link repair events to the same call's execution
|
|
167
|
+
outcome (did the repaired call succeed?) and compute recurrence-after-teach
|
|
168
|
+
per (model, failureMode). Aggregate per model into the table below. Wire
|
|
169
|
+
through the existing telemetry path (`core/telemetry.ts` / session-analytics)
|
|
170
|
+
rather than a new sink. The "round-trips saved" metric is simply the repaired
|
|
171
|
+
count; the "teach works" metric is recurrence decay.
|
|
172
|
+
|
|
173
|
+
## Model notes (living table; append, do not rewrite)
|
|
174
|
+
|
|
175
|
+
| Model | Observed modes | Notes | Date |
|
|
176
|
+
|---|---|---|---|
|
|
177
|
+
| glm (5.1) | 2 | stringified `edits` array on edit tool; was special-cased in `prepareEditArguments` | 2026-07-06 (from code comment) |
|
|
178
|
+
| deepseek-flash | 1,2,3,4 | full catalogue observed in external repair work | 2026-07 |
|
|
179
|
+
| deepseek v4 pro | 1,2,3,4 | same | 2026-07 |
|
|
180
|
+
| qwen | 1,2,3,4 | same | 2026-07 |
|
|
181
|
+
|
|
182
|
+
## Companion grammars (authoritative)
|
|
183
|
+
|
|
184
|
+
- `failure-grammar.md` — the `errorSignature → transform → guard → note`
|
|
185
|
+
table (modes 1–10 + bash/edit/read tool-specific rows). What `analyzer.ts`
|
|
186
|
+
classifies against and `registry.ts` encodes.
|
|
187
|
+
- `text-protocol-grammar.md` — the envelope EBNF, inbound variants, the
|
|
188
|
+
schema→primer projection, and the dictionary generated from pi's real
|
|
189
|
+
tools. What `text-protocol.ts` speaks and parses.
|
|
190
|
+
|
|
191
|
+
## Acceptance fixtures (minimum set per repair)
|
|
192
|
+
|
|
193
|
+
Per provider that feeds the choke point (anthropic, openai-completions,
|
|
194
|
+
openai-responses, google, bedrock, mistral): one fixture per failure-grammar
|
|
195
|
+
mode (1–10) proving repair, one proving the required-null bounce, one per
|
|
196
|
+
guard-fails case proving bounce (transform rejected, args untouched), one
|
|
197
|
+
proving well-formed args pass with NO mutation (hot path untouched), the
|
|
198
|
+
ordering fixture `'["a","b"]'` → `["a","b"]` not `['["a","b"]']`, and the
|
|
199
|
+
bash rows (`command` as argv-array → joined string; `command` as `{cmd}` →
|
|
200
|
+
string; `timeout:"30"` → 30; `timeout:{}` → omitted). Plus the microbench:
|
|
201
|
+
clean-path within noise of a bare `Check`; repaired-path under budget.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Text tool-call protocol grammar (the "phone" language)
|
|
2
|
+
|
|
3
|
+
The single source of truth for R38 (SPEAK primer + LISTEN parser) and R45
|
|
4
|
+
(calibration). ONE grammar, two consumers: the primer generator emits it to
|
|
5
|
+
the model, the parser recognizes it coming back. `text-protocol.ts` holds this
|
|
6
|
+
as code; this file is the human-readable spec it must match. If the two ever
|
|
7
|
+
disagree, the code's fixtures win and this file is corrected.
|
|
8
|
+
|
|
9
|
+
Design constraints: unambiguous to parse with a bounded scanner (no regex
|
|
10
|
+
soup, no nesting ambiguity), cheap to emit, and forgiving of the whitespace a
|
|
11
|
+
weak model adds. The envelope is line-orientable but not line-dependent.
|
|
12
|
+
|
|
13
|
+
## 1. Envelope grammar (EBNF)
|
|
14
|
+
|
|
15
|
+
```ebnf
|
|
16
|
+
call = open-tag , ws? , args-json , ws? , close-tag ;
|
|
17
|
+
open-tag = "<pi:call" , ws , 'name="' , tool-name , '"' , ws? , ">" ;
|
|
18
|
+
close-tag = "</pi:call>" ;
|
|
19
|
+
tool-name = ident ; (* must match a known tool *)
|
|
20
|
+
args-json = json-object ; (* strict JSON, RFC 8259 *)
|
|
21
|
+
ident = (letter | "_") , { letter | digit | "_" } ;
|
|
22
|
+
ws = { " " | "\t" | "\n" | "\r" } ;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- `args-json` is a COMPLETE JSON object (`{...}`), UTF-8, double-quoted keys.
|
|
26
|
+
It is NOT the harness's job to be a lenient JSON parser here — malformed
|
|
27
|
+
inner JSON is a LISTEN failure that flows into the R31 repair pipeline
|
|
28
|
+
(which owns JSON tolerance), not a grammar concern.
|
|
29
|
+
- One `<pi:call>...</pi:call>` per tool invocation. Multiple calls in one
|
|
30
|
+
turn = multiple envelopes; the parser extracts each independently in
|
|
31
|
+
document order.
|
|
32
|
+
- Text outside envelopes is prose (reasoning, explanation) and is preserved
|
|
33
|
+
as the assistant's text content — never discarded, never parsed as a call.
|
|
34
|
+
- The tag prefix `pi:` is deliberate: it is vanishingly unlikely to collide
|
|
35
|
+
with prose or code the model quotes, which keeps extraction unambiguous.
|
|
36
|
+
|
|
37
|
+
Why `<pi:call name="X">{...}</pi:call>` and not bare JSON or `X({...})`:
|
|
38
|
+
- an explicit open/close pair lets the scanner find exact boundaries even
|
|
39
|
+
when args-json contains `}` inside strings (scan to `</pi:call>`, then
|
|
40
|
+
parse the interior as JSON) — no brace-counting heuristics;
|
|
41
|
+
- `name=` as an attribute (not the JSON) means a malformed args body still
|
|
42
|
+
tells us WHICH tool was intended, so the R31 bounce/repair note can be
|
|
43
|
+
tool-specific;
|
|
44
|
+
- it is visually distinct from the code models love to emit, so calibration
|
|
45
|
+
(R45) can tell "spoke the protocol" from "wrote about the protocol".
|
|
46
|
+
|
|
47
|
+
## 2. Recognized inbound variants (LISTEN tolerance)
|
|
48
|
+
|
|
49
|
+
The parser accepts, in priority order, and normalizes all to the canonical
|
|
50
|
+
form above. Recognizing more than we emit is deliberate: open models arrive
|
|
51
|
+
pre-trained on other stacks' conventions.
|
|
52
|
+
|
|
53
|
+
1. Canonical `<pi:call name="X"> {json} </pi:call>`.
|
|
54
|
+
2. `<tool_call>{"name":"X","arguments":{...}}</tool_call>` (common OSS
|
|
55
|
+
convention; `arguments` may itself be a JSON STRING — that is exactly
|
|
56
|
+
R31 mode 2, handed straight to the repairer).
|
|
57
|
+
3. Fenced block tagged `tool`/`tool_call`:
|
|
58
|
+
```` ```tool\n{"name":"X","arguments":{...}}\n``` ````
|
|
59
|
+
Everything else is prose. Ambiguous text is NEVER guessed into a call
|
|
60
|
+
(doctrine: no heuristic soup). Unknown `tool-name` → not a call; it becomes a
|
|
61
|
+
bounce with the "unknown tool" note listing valid names.
|
|
62
|
+
|
|
63
|
+
## 3. Schema -> primer projection (how the dictionary is generated)
|
|
64
|
+
|
|
65
|
+
The primer is GENERATED from the live `Tool[]`, never hand-written, so adding
|
|
66
|
+
a tool updates the dictionary for free. Projection rules per tool:
|
|
67
|
+
|
|
68
|
+
- one line: `tool-name(arg:type[?], ...) - <one-line purpose from schema
|
|
69
|
+
description>`;
|
|
70
|
+
- `?` suffix marks Optional; enums render as `a|b|c`; arrays as `type[]`;
|
|
71
|
+
nested objects as `{k:type,...}`;
|
|
72
|
+
- types collapse to the five the model needs: `string`, `number`, `bool`,
|
|
73
|
+
`T[]`, `{...}`. No JSON-Schema keywords leak into the primer;
|
|
74
|
+
- required-args-first ordering; default values shown as `=default` when the
|
|
75
|
+
schema declares one;
|
|
76
|
+
- two worked examples per primer TOTAL (not per tool): one simplest call,
|
|
77
|
+
one call with an array arg (the shape open models most often break),
|
|
78
|
+
chosen from the actual tool set.
|
|
79
|
+
|
|
80
|
+
The primer header (fixed text):
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
To use a tool, reply with EXACTLY this and nothing else around it:
|
|
84
|
+
<pi:call name="TOOL">{ "arg": value }</pi:call>
|
|
85
|
+
Rules: arguments is ONE JSON object. Arrays are JSON arrays [ ], never
|
|
86
|
+
quoted strings. Omit optional args you do not need - do not send null.
|
|
87
|
+
Put any reasoning BEFORE the tag, not inside it.
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 4. The dictionary for pi's core tools (generated 2026-07-06)
|
|
91
|
+
|
|
92
|
+
Verbatim projection of the real schemas (`bash.ts`, `read.ts`, `edit.ts`,
|
|
93
|
+
`write.ts`, `ls.ts`, `grep.ts`, `find.ts`). This is what a text-only model
|
|
94
|
+
sees; regenerate whenever a tool schema changes (a test asserts this table
|
|
95
|
+
matches the projection).
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
bash(command:string, timeout:number?) - run a bash command
|
|
99
|
+
read(path:string, offset:number?, limit:number?, lineNumbers:bool?, tail:number?, filter:none|minimal|aggressive?) - read a file
|
|
100
|
+
edit(path:string, edits:{oldText:string,newText:string}[]) - apply targeted text replacements to a file
|
|
101
|
+
write(path:string, content:string) - write (create/overwrite) a file
|
|
102
|
+
ls(path:string?, limit:number?, metadata:bool?) - list a directory
|
|
103
|
+
grep(pattern:string, path:string?, glob:string?, ignoreCase:bool?, literal:bool?, context:number?, limit:number?) - search file contents
|
|
104
|
+
find(pattern:string, path:string?, limit:number?, ignoreCase:bool?) - find files by glob
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Worked examples (the two shipped in the primer):
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
Read a file:
|
|
111
|
+
<pi:call name="read">{ "path": "src/index.ts" }</pi:call>
|
|
112
|
+
|
|
113
|
+
Replace text (note edits is a JSON ARRAY of objects):
|
|
114
|
+
<pi:call name="edit">{ "path": "src/app.ts", "edits": [ { "oldText": "foo", "newText": "bar" } ] }</pi:call>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The `edit` example is deliberate: `edits` is the array field open models most
|
|
118
|
+
often send as a stringified array (`"[{...}]"`) or a bare object (`{...}`) —
|
|
119
|
+
showing the correct shape here, and repairing+teaching it via R31/R40 when
|
|
120
|
+
they still get it wrong, is the whole loop in one arg.
|
|
121
|
+
|
|
122
|
+
## 5. Parser output contract
|
|
123
|
+
|
|
124
|
+
`parseTextToolCalls(text, knownTools) -> { calls: ToolCallBlock[], text:
|
|
125
|
+
string }` where:
|
|
126
|
+
- `calls` are canonical toolCall blocks (name + parsed-or-raw args), each
|
|
127
|
+
tagged `origin: "text-protocol"` so R35 telemetry attributes them and R31
|
|
128
|
+
knows the args came through the phone;
|
|
129
|
+
- `text` is the input with every recognized envelope removed (the surviving
|
|
130
|
+
prose);
|
|
131
|
+
- args that fail JSON parse are passed to R31 as a raw string under the named
|
|
132
|
+
tool (repairer owns tolerance); the parser itself never invents args.
|
|
133
|
+
|
|
134
|
+
## 6. Calibration trials (R45) use this grammar
|
|
135
|
+
|
|
136
|
+
The trial tool is `echo(data:string)` (harness-provided, side-effect-free).
|
|
137
|
+
Calibration asks the model to `echo` a known token via the protocol and
|
|
138
|
+
checks the parser round-trips it. Grammar variants tried on failure, in
|
|
139
|
+
order: (a) restate header + one example; (b) drop to fenced-block variant 3
|
|
140
|
+
if angle-bracket tags fail; (c) shrink the dictionary to the single tool
|
|
141
|
+
under test. The variant that first round-trips is persisted per model (R46).
|