@caupulican/pi-adaptative 0.81.8 → 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.
Files changed (91) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/bundled-resources/skills/tool-call-repair/SKILL.md +148 -0
  3. package/dist/bundled-resources/skills/tool-call-repair/references/failure-grammar.md +128 -0
  4. package/dist/bundled-resources/skills/tool-call-repair/references/repair-catalogue.md +201 -0
  5. package/dist/bundled-resources/skills/tool-call-repair/references/text-protocol-grammar.md +141 -0
  6. package/dist/core/agent-session.d.ts +18 -0
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +198 -1
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/delegation/worker-actions.d.ts +2 -1
  11. package/dist/core/delegation/worker-actions.d.ts.map +1 -1
  12. package/dist/core/delegation/worker-actions.js +34 -2
  13. package/dist/core/delegation/worker-actions.js.map +1 -1
  14. package/dist/core/extensions/types.d.ts +3 -1
  15. package/dist/core/extensions/types.d.ts.map +1 -1
  16. package/dist/core/extensions/types.js.map +1 -1
  17. package/dist/core/failure-corpus.d.ts +27 -1
  18. package/dist/core/failure-corpus.d.ts.map +1 -1
  19. package/dist/core/failure-corpus.js +33 -11
  20. package/dist/core/failure-corpus.js.map +1 -1
  21. package/dist/core/model-registry.d.ts +2 -2
  22. package/dist/core/model-registry.d.ts.map +1 -1
  23. package/dist/core/model-registry.js +6 -4
  24. package/dist/core/model-registry.js.map +1 -1
  25. package/dist/core/models/adaptation-store.d.ts +62 -0
  26. package/dist/core/models/adaptation-store.d.ts.map +1 -0
  27. package/dist/core/models/adaptation-store.js +168 -0
  28. package/dist/core/models/adaptation-store.js.map +1 -0
  29. package/dist/core/session-analytics.d.ts +24 -1
  30. package/dist/core/session-analytics.d.ts.map +1 -1
  31. package/dist/core/session-analytics.js +75 -0
  32. package/dist/core/session-analytics.js.map +1 -1
  33. package/dist/core/settings-manager.d.ts +6 -0
  34. package/dist/core/settings-manager.d.ts.map +1 -1
  35. package/dist/core/settings-manager.js.map +1 -1
  36. package/dist/core/slash-commands.d.ts.map +1 -1
  37. package/dist/core/slash-commands.js +5 -0
  38. package/dist/core/slash-commands.js.map +1 -1
  39. package/dist/core/system-prompt-builder.d.ts +4 -0
  40. package/dist/core/system-prompt-builder.d.ts.map +1 -1
  41. package/dist/core/system-prompt-builder.js +8 -0
  42. package/dist/core/system-prompt-builder.js.map +1 -1
  43. package/dist/core/tool-repair-health.d.ts +3 -0
  44. package/dist/core/tool-repair-health.d.ts.map +1 -0
  45. package/dist/core/tool-repair-health.js +43 -0
  46. package/dist/core/tool-repair-health.js.map +1 -0
  47. package/dist/core/tool-repair-settings.d.ts +11 -0
  48. package/dist/core/tool-repair-settings.d.ts.map +1 -0
  49. package/dist/core/tool-repair-settings.js +18 -0
  50. package/dist/core/tool-repair-settings.js.map +1 -0
  51. package/dist/core/tools/edit.d.ts.map +1 -1
  52. package/dist/core/tools/edit.js +0 -9
  53. package/dist/core/tools/edit.js.map +1 -1
  54. package/dist/core/tools/find.d.ts.map +1 -1
  55. package/dist/core/tools/find.js +10 -7
  56. package/dist/core/tools/find.js.map +1 -1
  57. package/dist/core/tools/grep.d.ts.map +1 -1
  58. package/dist/core/tools/grep.js +15 -9
  59. package/dist/core/tools/grep.js.map +1 -1
  60. package/dist/modes/interactive/components/tool-execution.d.ts +7 -3
  61. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  62. package/dist/modes/interactive/components/tool-execution.js +21 -4
  63. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  64. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  65. package/dist/modes/interactive/external-editor.js +5 -3
  66. package/dist/modes/interactive/external-editor.js.map +1 -1
  67. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  68. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  69. package/dist/modes/interactive/interactive-mode.js +37 -7
  70. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  71. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  72. package/dist/modes/rpc/rpc-mode.js +8 -0
  73. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  74. package/dist/modes/rpc/rpc-types.d.ts +24 -0
  75. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  76. package/dist/modes/rpc/rpc-types.js.map +1 -1
  77. package/docs/index.md +1 -0
  78. package/docs/rpc.md +36 -0
  79. package/docs/settings.md +20 -0
  80. package/docs/tool-repair.md +75 -0
  81. package/docs/usage.md +2 -0
  82. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  83. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  84. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  85. package/examples/extensions/sandbox/package-lock.json +2 -2
  86. package/examples/extensions/sandbox/package.json +1 -1
  87. package/examples/extensions/with-deps/package-lock.json +2 -2
  88. package/examples/extensions/with-deps/package.json +1 -1
  89. package/npm-shrinkwrap.json +12 -12
  90. package/package.json +6 -4
  91. package/docs/bug-ledger.md +0 -131
package/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
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
+
22
+ ## [0.81.9] - 2026-07-07
23
+
1
24
  ## [0.81.8] - 2026-07-07
2
25
 
3
26
  ### Fixed
@@ -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).