@r3b1s/pi-repair-layer 0.2.1 → 0.4.0
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/README.md +173 -293
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/{index.ts → dist/index.js} +2 -1
- package/dist/index.js.map +1 -0
- package/dist/src/core.d.ts +8 -0
- package/dist/src/core.d.ts.map +1 -0
- package/dist/src/core.js +7 -0
- package/dist/src/core.js.map +1 -0
- package/dist/src/envelope.d.ts +21 -0
- package/dist/src/envelope.d.ts.map +1 -0
- package/dist/src/envelope.js +158 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/grammar-recovery.d.ts +105 -0
- package/dist/src/grammar-recovery.d.ts.map +1 -0
- package/dist/src/grammar-recovery.js +1052 -0
- package/dist/src/grammar-recovery.js.map +1 -0
- package/dist/src/grammar.d.ts +2 -0
- package/dist/src/grammar.d.ts.map +1 -0
- package/dist/src/grammar.js +2 -0
- package/dist/src/grammar.js.map +1 -0
- package/dist/src/index.d.ts +36 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +558 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lifecycle.d.ts +34 -0
- package/dist/src/lifecycle.d.ts.map +1 -0
- package/dist/src/lifecycle.js +133 -0
- package/dist/src/lifecycle.js.map +1 -0
- package/dist/src/pi.d.ts +19 -0
- package/dist/src/pi.d.ts.map +1 -0
- package/dist/src/pi.js +38 -0
- package/dist/src/pi.js.map +1 -0
- package/dist/src/pipeline.d.ts +10 -0
- package/dist/src/pipeline.d.ts.map +1 -0
- package/dist/src/pipeline.js +166 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/policy.d.ts +16 -0
- package/dist/src/policy.d.ts.map +1 -0
- package/dist/src/policy.js +31 -0
- package/dist/src/policy.js.map +1 -0
- package/dist/src/preprocess.d.ts +37 -0
- package/dist/src/preprocess.d.ts.map +1 -0
- package/dist/src/preprocess.js +216 -0
- package/dist/src/preprocess.js.map +1 -0
- package/dist/src/repair-engine.d.ts +91 -0
- package/dist/src/repair-engine.d.ts.map +1 -0
- package/dist/src/repair-engine.js +457 -0
- package/dist/src/repair-engine.js.map +1 -0
- package/dist/src/settings.d.ts +38 -0
- package/dist/src/settings.d.ts.map +1 -0
- package/dist/src/settings.js +87 -0
- package/dist/src/settings.js.map +1 -0
- package/dist/src/tables.d.ts +16 -0
- package/dist/src/tables.d.ts.map +1 -0
- package/dist/src/tables.js +302 -0
- package/dist/src/tables.js.map +1 -0
- package/dist/src/types.d.ts +52 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/value-strips.d.ts +52 -0
- package/dist/src/value-strips.d.ts.map +1 -0
- package/dist/src/value-strips.js +191 -0
- package/dist/src/value-strips.js.map +1 -0
- package/docs/how-it-works.md +227 -0
- package/docs/operations.md +141 -0
- package/docs/research.md +390 -0
- package/docs/tool-owner-integration.md +557 -0
- package/package.json +31 -6
- package/src/grammar-recovery.ts +0 -1269
- package/src/index.ts +0 -621
- package/src/repair-engine.ts +0 -518
- package/src/settings.ts +0 -95
- package/src/tables.ts +0 -184
- package/src/value-strips.ts +0 -218
package/README.md
CHANGED
|
@@ -1,42 +1,46 @@
|
|
|
1
1
|
# pi-repair-layer
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
3
|
+
pi-repair-layer repairs the small, predictable tool-call mistakes that can
|
|
4
|
+
derail an otherwise productive [pi](https://github.com/earendil-works/pi) turn.
|
|
5
|
+
Install it for pi's built-in tools, or follow the
|
|
6
|
+
[tool-owner integration guide](docs/tool-owner-integration.md) to bring the
|
|
7
|
+
same repair pipeline to tools in your own extension.
|
|
8
|
+
|
|
9
|
+
No LLM calls. No network requests. No uploaded telemetry.
|
|
10
|
+
|
|
11
|
+
## Contents
|
|
12
|
+
|
|
13
|
+
- [Why use it?](#why-use-it)
|
|
14
|
+
- [Install](#install)
|
|
15
|
+
- [Terms in plain English](#terms-in-plain-english)
|
|
16
|
+
- [What it repairs](#what-it-repairs)
|
|
17
|
+
- [Safe by design](#safe-by-design)
|
|
18
|
+
- [Use it in your own extension](#use-it-in-your-own-extension)
|
|
19
|
+
- [Settings and local telemetry](#settings-and-local-telemetry)
|
|
20
|
+
- [Documentation](#documentation)
|
|
21
|
+
- [Prior art](#prior-art)
|
|
22
|
+
- [Limitations](#limitations)
|
|
23
|
+
- [Development](#development)
|
|
24
|
+
|
|
25
|
+
## Why use it?
|
|
26
|
+
|
|
27
|
+
Models often understand the task but miss a detail of the tool contract:
|
|
28
|
+
|
|
29
|
+
| Model sends | Tool expects |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `{file_path: "/x"}` | `{path: "/x"}` |
|
|
32
|
+
| `{include: "src"}` | `{include: ["src"]}` |
|
|
33
|
+
| `{offset: null}` | omit optional `offset` |
|
|
34
|
+
| `'{"command":"ls"}'` | `{command: "ls"}` |
|
|
35
|
+
| flat `old_string` / `new_string` fields | an `edits` array |
|
|
36
|
+
|
|
37
|
+
Without a pre-validation repair seam, pi may reject the call or convert a bad
|
|
38
|
+
value into valid-looking garbage. pi-repair-layer fixes only configured or
|
|
39
|
+
schema-proven cases, validates the result, executes it, and returns a
|
|
40
|
+
`<repair_note>` so the model learns the correct shape for its next call.
|
|
41
|
+
|
|
42
|
+
It covers pi's `read`, `bash`, `edit`, `write`, `grep`, `find`, and `ls` tools.
|
|
43
|
+
Already-valid input takes the fast path and is returned untouched.
|
|
40
44
|
|
|
41
45
|
## Install
|
|
42
46
|
|
|
@@ -44,285 +48,161 @@ Plain-English first, precise meaning after — for readers new to LLM tool-calli
|
|
|
44
48
|
pi install npm:@r3b1s/pi-repair-layer
|
|
45
49
|
```
|
|
46
50
|
|
|
47
|
-
Or from
|
|
51
|
+
Or install directly from GitHub:
|
|
48
52
|
|
|
49
53
|
```bash
|
|
50
54
|
pi install git:github.com/r3b1s/pi-repair-layer
|
|
51
55
|
```
|
|
52
56
|
|
|
53
|
-
pi
|
|
54
|
-
|
|
57
|
+
pi will show a one-time warning that built-in tools were overridden. The
|
|
58
|
+
extension reuses pi's real schemas, executors, renderers, and runtime modules;
|
|
59
|
+
it adds the pre-validation repair step.
|
|
60
|
+
|
|
61
|
+
## Terms in plain English
|
|
62
|
+
|
|
63
|
+
- **Tool call** — the model asking the agent to run a tool with structured arguments.
|
|
64
|
+
- **Schema / validation** — the rules for those arguments and the check that they fit.
|
|
65
|
+
- **Silent coercion** — a bad value being converted into a valid-looking but unintended value.
|
|
66
|
+
- **Grammar leak** — the model printing a tool call as text instead of making the call.
|
|
67
|
+
- **Anchor bleed** — stray `^` or `$` grammar characters attached to a non-regex value.
|
|
68
|
+
- **Phantom tool call** — a tool-use signal that contains no actual call to run.
|
|
69
|
+
- **Repair note** — a short explanation returned to the model after a repair.
|
|
70
|
+
- **Fast path** — returning valid input without changing or cloning it.
|
|
71
|
+
- **Path selector** — an address such as `/path` or `/edits/*/oldText` for one configured location.
|
|
72
|
+
- **Preprocessor** — an explicit cleanup that runs at a path selector before validation.
|
|
73
|
+
- **Invariant** — a safety rule that must remain true for every input.
|
|
74
|
+
- **Fail closed** — rejecting input when the pipeline cannot prove a safe, valid repair.
|
|
55
75
|
|
|
56
|
-
|
|
57
|
-
`typebox` to the running instance's own modules, so the extension always binds
|
|
58
|
-
to your live pi and its exact validator version. The `node_modules/` here is
|
|
59
|
-
only used for the tests and `tsc`.
|
|
76
|
+
## What it repairs
|
|
60
77
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
The default adaptive policy handles:
|
|
79
|
+
|
|
80
|
+
- exact field aliases such as `file_path` → `path`, `cmd` → `command`, and
|
|
81
|
+
`old_string` → `oldText`;
|
|
82
|
+
- JSON-stringified objects and arrays, singleton object envelopes, and bare
|
|
83
|
+
strings where a configured object or array is expected;
|
|
84
|
+
- invalid optional `null` values and empty object placeholders;
|
|
85
|
+
- markdown auto-links in configured filesystem paths;
|
|
86
|
+
- the legacy flat edit shape used by several coding agents; and
|
|
87
|
+
- model-gated anchor bleed and leaked argument tokens at configured locations.
|
|
88
|
+
|
|
89
|
+
Unknown keys are not fuzzily renamed or generically deleted. See
|
|
90
|
+
[How repair works](docs/how-it-works.md) for the ordered pipeline, complete
|
|
91
|
+
repair catalog, grammar handling, and the reasoning behind the hook placement.
|
|
92
|
+
|
|
93
|
+
## Safe by design
|
|
94
|
+
|
|
95
|
+
The pipeline recovers the outer argument envelope, chains the tool owner's
|
|
96
|
+
compatibility hook, applies explicitly scoped preprocessing, validates strictly,
|
|
97
|
+
repairs only reported schema issues, and validates again before returning a
|
|
98
|
+
repaired result.
|
|
99
|
+
|
|
100
|
+
- Valid input stays untouched unless an explicitly configured valid-value
|
|
101
|
+
transform applies.
|
|
102
|
+
- Every mutation carries a stable rule ID and model-facing note.
|
|
103
|
+
- Repair work is bounded, deterministic, and covered by property and seeded-fuzz tests.
|
|
104
|
+
- Unrepairable input produces a model-readable retry error rather than `{}` or guessed values.
|
|
105
|
+
- Telemetry stays local and excludes arguments, paths, commands, content, and note text.
|
|
106
|
+
- Turning assistant text into an executable tool call always requires explicit `recover` mode.
|
|
107
|
+
|
|
108
|
+
| Profile | Tool arguments | Grammar text | Promotion |
|
|
109
|
+
|---|---|---|---|
|
|
110
|
+
| `conservative` | exact, schema-guided repair | observe only | never |
|
|
111
|
+
| `adaptive` (default) | adds validated/model-gated recovery | strip known tools | never |
|
|
112
|
+
| `recover` | same as adaptive | strip known tools | safety-gated |
|
|
113
|
+
|
|
114
|
+
Unknown or unavailable tool grammar is preserved by default in every profile.
|
|
115
|
+
The [behavior and safety reference](docs/how-it-works.md) explains the gates and
|
|
116
|
+
invariants in detail.
|
|
117
|
+
|
|
118
|
+
## Use it in your own extension
|
|
119
|
+
|
|
120
|
+
Tools registered by other extensions are not intercepted automatically: their
|
|
121
|
+
owner must opt in at the `prepareArguments` boundary.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { adaptToolDefinition } from "@r3b1s/pi-repair-layer/pi";
|
|
125
|
+
|
|
126
|
+
pi.registerTool(
|
|
127
|
+
adaptToolDefinition(definition, {
|
|
128
|
+
policy: "adaptive",
|
|
129
|
+
preprocessors: [
|
|
130
|
+
{ kind: "alias", selector: "/path", aliases: ["file_path"] },
|
|
131
|
+
],
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
64
134
|
```
|
|
65
135
|
|
|
66
|
-
The
|
|
67
|
-
|
|
136
|
+
The adapter chains an existing owner hook and fails closed by default. The
|
|
137
|
+
[full integration guide](docs/tool-owner-integration.md) covers selectors,
|
|
138
|
+
custom transforms, the side-effect-free `/core` API, call-ID correlation,
|
|
139
|
+
`<repair_note>` attachment, privacy, and integration tests.
|
|
68
140
|
|
|
69
|
-
|
|
141
|
+
Public subpaths are compiled ESM with declarations and source maps:
|
|
70
142
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
| `dropNullOrUndefinedField` | `{path: "/x", offset: null}` | `{path: "/x"}` |
|
|
75
|
-
| `dropEmptyObjectPlaceholder` | `{tags: {}}` where array expected | field dropped |
|
|
76
|
-
| `parseJsonStringifiedArray` | `{include: '["a","b"]'}` | `{include: ["a","b"]}` |
|
|
77
|
-
| `parseJsonStringifiedObject` | stringified object for object field | parsed |
|
|
78
|
-
| `wrapBareStringAsArray` | `{include: "foo"}` | `{include: ["foo"]}` |
|
|
79
|
-
| `wrapRootStringAsObject` | `"echo hi"` as the whole input to `bash` | `{command: "echo hi"}` |
|
|
80
|
-
| `parseJsonStringifiedRootObject` | `'{"command":"ls"}'` as the whole input | parsed |
|
|
81
|
-
| `unwrapMarkdownAutoLink` | `path: "/x/[notes.md](http://notes.md)"` | `path: "/x/notes.md"` |
|
|
82
|
-
| `foldFlatEditFields` | `{path, old_string, new_string}` for `edit` | `{path, edits: [{oldText, newText}]}` |
|
|
83
|
-
|
|
84
|
-
Alias tables cover the contracts models actually leak: Claude Code's
|
|
85
|
-
`file_path`/`old_string`/`new_string`, aider's `search`/`replace`, generic
|
|
86
|
-
`cmd`/`query`/`text`/`contents`, at any nesting depth (e.g. inside `edits[n]`).
|
|
87
|
-
|
|
88
|
-
Every repair is surfaced to the model as a `<repair_note>` prefixed to the tool
|
|
89
|
-
result, e.g.:
|
|
143
|
+
- `@r3b1s/pi-repair-layer/pi` — tool-owner adapter;
|
|
144
|
+
- `@r3b1s/pi-repair-layer/core` — pure pipeline, policy, lifecycle, and formatting APIs; and
|
|
145
|
+
- `@r3b1s/pi-repair-layer/grammar` — pure grammar parsing and recovery helpers.
|
|
90
146
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
147
|
+
Node 22+ is supported; pi 0.80.6 is the verified baseline. Documented exports
|
|
148
|
+
follow semantic versioning, and the existing `repairToolInput` API remains
|
|
149
|
+
supported for the current major.
|
|
94
150
|
|
|
95
|
-
|
|
96
|
-
itself on the next turn.
|
|
97
|
-
|
|
98
|
-
## Model-gated value strips
|
|
99
|
-
|
|
100
|
-
Before the validate-then-repair engine runs, a small pre-pass
|
|
101
|
-
(`src/value-strips.ts`) cleans two model-specific artifacts that are *valid
|
|
102
|
-
strings* and so slip past validation entirely:
|
|
103
|
-
|
|
104
|
-
- **Anchor bleed** — leading `^` / trailing `$` bled into a value
|
|
105
|
-
(`read {path: "^/x$"}` → `{path: "/x"}`).
|
|
106
|
-
- **Grammar-token leaks** — GLM `<arg_key>`/`<arg_value>` markers stuck onto
|
|
107
|
-
object keys or values (`{"<arg_key>pattern</arg_key>": "<arg_value>foo</arg_value>"}`
|
|
108
|
-
→ `{pattern: "foo"}`).
|
|
109
|
-
|
|
110
|
-
Both are gated on the current model id — anchor bleed on `kimi-k2` / `minimax` /
|
|
111
|
-
`glm`, grammar tokens on `glm` — since these are model-specific quirks. Each
|
|
112
|
-
strip emits a `<repair_note>` and telemetry exactly like an engine repair, and
|
|
113
|
-
the strips are adapted from [pi-tool-repair](#prior-art). One improvement over
|
|
114
|
-
upstream: the anchor strip **skips `grep.pattern`**, the one built-in field
|
|
115
|
-
that is a real regex, where a `^`/`$` may be intended syntax and is
|
|
116
|
-
indistinguishable from a bled anchor — so it is never guessed at.
|
|
117
|
-
(`find.pattern` is a glob, not a regex, so it is *not* exempt.) Whether anchor
|
|
118
|
-
bleed happens on pi at all is an open empirical question; shipping the strips
|
|
119
|
-
instrumented answers it for free.
|
|
120
|
-
|
|
121
|
-
## Grammar-leak recovery (opt-in)
|
|
122
|
-
|
|
123
|
-
When a model prints a tool call as text (a [grammar leak](#glossary)) instead of
|
|
124
|
-
emitting a real one, `src/grammar-recovery.ts` (adapted from
|
|
125
|
-
[pi-tool-repair](#prior-art), 10 grammar families, code-fence-aware) handles it
|
|
126
|
-
on pi's `message_end` hook. Modes, set via `/repair-settings`:
|
|
127
|
-
|
|
128
|
-
- **`off`** — never touch assistant text.
|
|
129
|
-
- **`strip`** (default) — remove the leaked grammar from the text on gated
|
|
130
|
-
models, but never promote it to an executable call.
|
|
131
|
-
- **`recover`** — additionally promote the parsed call to a real tool call that
|
|
132
|
-
executes the same turn and re-enters this layer's `prepareArguments` repair
|
|
133
|
-
path. A recovery note is stashed so the executed call surfaces
|
|
134
|
-
`<repair_note>recovered a leaked … tool call…</repair_note>`.
|
|
135
|
-
|
|
136
|
-
Because promotion turns model *text* into *execution*, it is guarded: opt-in
|
|
137
|
-
`recover` mode, a known-tool allowlist, an empty-arguments skip, role
|
|
138
|
-
preservation, and a **stopReason gate** — a call is promoted only when the
|
|
139
|
-
original message's `stopReason` is `"stop"`. Upstream promotes regardless and
|
|
140
|
-
overwrites `stopReason: "length"`, which would defeat pi's protection that fails
|
|
141
|
-
all tool calls on truncated output ([research.md Claim 7][r-claim7]). Stripping
|
|
142
|
-
leaked text is allowed on any stopReason; only promotion is gated.
|
|
143
|
-
|
|
144
|
-
## Design: validate-then-repair, hooked pre-validation
|
|
145
|
-
|
|
146
|
-
pi's agent loop runs, per tool call ([research.md Claim 1][r-claim1]):
|
|
151
|
+
## Settings and local telemetry
|
|
147
152
|
|
|
148
|
-
|
|
149
|
-
tool.prepareArguments(raw) → validateToolArguments() → tool_call event → execute
|
|
150
|
-
```
|
|
153
|
+
All telemetry stays on your machine and is used only for debugging and tracing tool-call repairs; nothing is sent or uploaded.
|
|
151
154
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Two consequences drove the architecture:
|
|
160
|
-
|
|
161
|
-
1. **The `tool_call` extension event fires *after* validation**
|
|
162
|
-
([research.md Claims 1–2][r-claim1]). A validation
|
|
163
|
-
failure short-circuits to an error result before any event handler runs, so
|
|
164
|
-
an extension listening on `tool_call` can never see — let alone repair —
|
|
165
|
-
malformed input. (This is why repair extensions built on that hook can't
|
|
166
|
-
work for built-in tools.) The only pre-validation seam is
|
|
167
|
-
`prepareArguments`, which extensions reach by overriding a built-in tool via
|
|
168
|
-
`pi.registerTool({ same name })`. Each override here spreads the original
|
|
169
|
-
definition (`createReadToolDefinition(cwd)` etc. are exported from pi's
|
|
170
|
-
root), so renderers, prompt metadata, and execution are the real built-ins;
|
|
171
|
-
only `prepareArguments` is chained and `execute` thinly wrapped for notes.
|
|
172
|
-
|
|
173
|
-
2. **pi's own coercion (`Value.Convert`) silently corrupts the classic failure
|
|
174
|
-
modes.** Measured on typebox 1.1.38, which pi validates with:
|
|
175
|
-
|
|
176
|
-
| Model sends | Convert produces | Then |
|
|
177
|
-
|---|---|---|
|
|
178
|
-
| `'["a","b"]'` for an array | `['["a","b"]']` | passes validation, executes with garbage |
|
|
179
|
-
| `null` for an optional string | `"null"` | passes, e.g. greps for the string `null` |
|
|
180
|
-
| `null` for a required `path` | `"null"` | passes, reads a file named `null` |
|
|
181
|
-
| `null` for an optional number | `0` | passes, offset silently zero |
|
|
182
|
-
|
|
183
|
-
So this engine checks **strictly first** (no Convert) and repairs at the
|
|
184
|
-
strict-error sites before Convert can mangle them. Benign coercions
|
|
185
|
-
(`"5"` → `5`) still fall through to pi's native behavior untouched. The
|
|
186
|
-
repair layer therefore doesn't just recover calls that would have errored —
|
|
187
|
-
it fixes calls pi currently executes *wrongly*.
|
|
188
|
-
|
|
189
|
-
The engine itself follows the validate-then-repair shape commandcode's author
|
|
190
|
-
described publicly: parse as-is; if it succeeds, ship it untouched (fast path
|
|
191
|
-
returns the original object by reference); on failure, walk the validator's own
|
|
192
|
-
issue list and spend repair budget only at the paths the schema disagreed with;
|
|
193
|
-
re-validate; never ship a repair that doesn't verify. Rule order is fixed —
|
|
194
|
-
JSON-array parsing before bare-string wrapping, renames before null-drops.
|
|
195
|
-
|
|
196
|
-
The one deliberate preprocess (not validate-then-repair) is markdown auto-link
|
|
197
|
-
unwrapping on path fields, because `"/x/[notes.md](http://notes.md)"` is a
|
|
198
|
-
perfectly valid string that validation can never flag. It unwraps only the
|
|
199
|
-
degenerate case where the link text equals the URL minus its protocol; real
|
|
200
|
-
markdown links pass through, and content fields are never touched.
|
|
201
|
-
|
|
202
|
-
## Telemetry (local only)
|
|
203
|
-
|
|
204
|
-
Repair outcomes append to `~/.pi/agent/tool-repair/telemetry.jsonl`: timestamp,
|
|
205
|
-
tool, model id, outcome, rules fired, an issue summary, and an FNV-1a
|
|
206
|
-
fingerprint of the (tool, failure-shape) pair — the same shape-fingerprint
|
|
207
|
-
trick commandcode uses to count distinct failure signatures. Inputs themselves
|
|
208
|
-
are never logged.
|
|
209
|
-
|
|
210
|
-
Records come on two channels. The **tool channel** (the default; records with no
|
|
211
|
-
`channel` field, so old logs read unchanged) keys on a tool, with `outcome` one
|
|
212
|
-
of `repaired`, `unrepairable`, or `recovered` (a promoted grammar-leak call).
|
|
213
|
-
The **message channel** (`channel: "message"`) records grammar strip-only events
|
|
214
|
-
— a leak removed with nothing promoted, so there is no tool to key on — with the
|
|
215
|
-
grammar family that was stripped. `/repair-stats` summarizes both:
|
|
155
|
+
- `/repair-settings` changes the policy, grammar mode, unknown-tool text policy,
|
|
156
|
+
TUI indicator, and visible repair notes.
|
|
157
|
+
- `/repair-stats` summarizes local repair outcomes by model, tool, and rule.
|
|
158
|
+
- `PI_TOOL_REPAIR_LOG=1` prints decisions to stderr.
|
|
159
|
+
- `PI_TOOL_REPAIR_TELEMETRY=off` disables local telemetry.
|
|
160
|
+
- `PI_TOOL_REPAIR_PASSTHROUGH=1` restores pi's native behavior for unrepairable input.
|
|
216
161
|
|
|
217
|
-
|
|
218
|
-
/
|
|
219
|
-
|
|
162
|
+
Settings persist under `~/.pi/agent/tool-repair/`. See the
|
|
163
|
+
[operations guide](docs/operations.md) for paths, record contents, privacy,
|
|
164
|
+
diagnostics, and verification commands.
|
|
220
165
|
|
|
221
|
-
|
|
222
|
-
family, and a rules-fired tally. Watch per-model rates to catch a model
|
|
223
|
-
regressing on a specific contract.
|
|
166
|
+
## Documentation
|
|
224
167
|
|
|
225
|
-
|
|
226
|
-
|
|
168
|
+
- [Integrating extension-owned tools](docs/tool-owner-integration.md)
|
|
169
|
+
- [How repair works: behavior, safety, and limitations](docs/how-it-works.md)
|
|
170
|
+
- [Operations, settings, telemetry, and verification](docs/operations.md)
|
|
171
|
+
- [Source-backed pi integration research](docs/research.md)
|
|
227
172
|
|
|
228
|
-
##
|
|
173
|
+
## Prior art
|
|
229
174
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
path end-to-end — streaming, preflight, validation, repair, execution, and note
|
|
235
|
-
surfacing — and passes against pi 0.80.6:
|
|
175
|
+
The value-strip rules and grammar-recovery approach originate in Tom X Nguyen's
|
|
176
|
+
MIT-licensed [`monotykamary/pi-tool-repair`](https://github.com/monotykamary/pi-tool-repair).
|
|
177
|
+
This project adapted its anchor/token cleanup and grammar parsers, then scoped
|
|
178
|
+
the transforms, added reporting, and gated executable promotion.
|
|
236
179
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
180
|
+
The key integration difference is mechanical: pi runs
|
|
181
|
+
`prepareArguments → validation → tool_call → execute`. A malformed call fails
|
|
182
|
+
before `tool_call`, so this extension hooks the owning tool's
|
|
183
|
+
`prepareArguments`; the sequence and propagation behavior are verified in
|
|
184
|
+
[research Claims 1–4](docs/research.md#claim-1--loop-ordering-preparearguments-runs-before-validation-which-runs-before-the-tool_call-event).
|
|
185
|
+
See [How repair works](docs/how-it-works.md#prior-art-and-hook-placement) for the
|
|
186
|
+
full adaptation notes and comparison.
|
|
240
187
|
|
|
241
|
-
|
|
242
|
-
custom provider finished registering, so this hung; 0.80.6 streams through the
|
|
243
|
-
registered provider correctly.) The `test/upstream-drift.test.ts` suite drives
|
|
244
|
-
the same real loop fully in-process (via pi-ai's faux provider), so the loop
|
|
245
|
-
behaviors this layer depends on are covered by `pnpm test` too, without the
|
|
246
|
-
`pi` binary.
|
|
247
|
-
|
|
248
|
-
## Display settings
|
|
249
|
-
|
|
250
|
-
Repaired tool calls get a `🔨 ✓ input repaired (rules...)` line appended to
|
|
251
|
-
their result row in the TUI; optionally the repair note text renders beneath
|
|
252
|
-
it. `/repair-settings` toggles the indicator and the note text independently,
|
|
253
|
-
and cycles the grammar-leak recovery mode (`off → strip → recover`; default
|
|
254
|
-
`strip`). All three persist to `~/.pi/agent/tool-repair/settings.json` (or
|
|
255
|
-
`PI_TOOL_REPAIR_SETTINGS=<path>`), alongside an optional `grammarAllowedTools`
|
|
256
|
-
list that restricts which tool names a leaked call may be promoted to (when
|
|
257
|
-
empty, the active tool set is used). Indicators are persisted per session via
|
|
258
|
-
custom entries, so they survive `/reload` and session resume.
|
|
259
|
-
|
|
260
|
-
## Unrepairable input
|
|
261
|
-
|
|
262
|
-
When repairs can't make the input valid, the layer raises a model-readable
|
|
263
|
-
retry error (`Invalid input for tool "write". Fix these issues and retry: ...`)
|
|
264
|
-
instead of passing the input through — passing through would let pi's
|
|
265
|
-
`Value.Convert` coerce it into garbage (`content: null` becomes the literal
|
|
266
|
-
string `"null"` on disk) and execute anyway. Set `PI_TOOL_REPAIR_PASSTHROUGH=1`
|
|
267
|
-
to restore pi's native behavior.
|
|
188
|
+
## Limitations
|
|
268
189
|
|
|
269
|
-
|
|
190
|
+
- Another extension's tools require owner opt-in through `/pi` or `/core`.
|
|
191
|
+
- Two extensions overriding the same built-in do not compose; load order wins.
|
|
192
|
+
- Similar but unconfigured aliases are not guessed.
|
|
193
|
+
- Phantom tool calls are not synthesized into calls.
|
|
194
|
+
- Grammar promotion is limited to recognized, non-empty calls for active or
|
|
195
|
+
explicitly allowed tools and never runs on truncated output.
|
|
270
196
|
|
|
271
|
-
|
|
272
|
-
[`monotykamary/pi-tool-repair`](https://github.com/monotykamary/pi-tool-repair)
|
|
273
|
-
(MIT), Tom X Nguyen's pi extension. What this project adapted from it:
|
|
274
|
-
|
|
275
|
-
- The anchor-bleed and grammar-token-leak strips (`src/value-strips.ts`),
|
|
276
|
-
reworked to run inside `prepareArguments`, to skip the `grep.pattern` regex
|
|
277
|
-
field, and to report through this layer's repair-note / telemetry machinery.
|
|
278
|
-
- The grammar-leak recovery parsers, candidate selection, code-fence handling,
|
|
279
|
-
and range removal (`src/grammar-recovery.ts`, ~10 grammar families), adapted
|
|
280
|
-
in place with a one-line provenance header, plus the new `stopReason`
|
|
281
|
-
promotion gate.
|
|
282
|
-
|
|
283
|
-
The two projects also share a common Command Code lineage in their
|
|
284
|
-
validate-then-repair engines (near-identical rule names and alias tables).
|
|
285
|
-
|
|
286
|
-
Where they differ is **which hook reaches a malformed built-in call**, and that
|
|
287
|
-
is mechanical, not a matter of quality. pi runs
|
|
288
|
-
`prepareArguments → validateToolArguments → tool_call event → execute`
|
|
289
|
-
([research.md Claim 1][r-claim1]). A malformed built-in call makes
|
|
290
|
-
`validateToolArguments` **throw** before the `tool_call` event ever fires
|
|
291
|
-
([Claim 2][r-claim1]), so a repair that keys on `tool_call` cannot see it; and
|
|
292
|
-
even for a call that does pass, the `tool_call` event only propagates a handler's
|
|
293
|
-
*in-place* mutation of `event.input`, never a reassignment ([Claim 3][r-claim3]).
|
|
294
|
-
pi-tool-repair's built-in repair keys on `tool_call`; this project overrides the
|
|
295
|
-
built-in tool so its repair runs in `prepareArguments`, the one seam ahead of
|
|
296
|
-
validation. That is why the two hooks reach different cases — see the cited
|
|
297
|
-
research entries for the source. Conversely, the capabilities assimilated here
|
|
298
|
-
(value strips, grammar recovery) live on hooks that *do* propagate their results
|
|
299
|
-
(`prepareArguments`, `message_end` — [Claim 4][r-claim4]), which is why adapting
|
|
300
|
-
them is sound.
|
|
301
|
-
|
|
302
|
-
[r-claim1]: docs/research.md#claim-1--loop-ordering-preparearguments-runs-before-validation-which-runs-before-the-tool_call-event
|
|
303
|
-
[r-claim3]: docs/research.md#claim-3--tool_call-event-in-place-input-mutation-propagates-reassignment-does-not
|
|
304
|
-
[r-claim4]: docs/research.md#claim-4--message_end-replacement-mutates-the-loops-message-in-place-listeners-are-awaited-and-the-replacement-executes-same-turn-with-a-role-guard
|
|
305
|
-
[r-claim7]: docs/research.md#claim-7--stopreason-length-causes-pi-to-fail-all-tool-calls-in-the-message
|
|
197
|
+
## Development
|
|
306
198
|
|
|
307
|
-
|
|
199
|
+
```bash
|
|
200
|
+
pnpm install
|
|
201
|
+
mise run ci # typecheck + lint + tests
|
|
202
|
+
pnpm run test:package # packed clean-consumer smoke test
|
|
203
|
+
pnpm run test:fuzz # deterministic bounded fuzz campaign
|
|
204
|
+
test/run-chaos.sh # scripted real pi-loop exercise
|
|
205
|
+
```
|
|
308
206
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
they can import `repairToolInput` from `src/repair-engine.ts` and use it in
|
|
312
|
-
their own `prepareArguments`.
|
|
313
|
-
- Wrong-but-optional fields are invisible to validation by design: pi's schemas
|
|
314
|
-
allow extra keys, so `grep {pattern, directory: "src"}` validates and
|
|
315
|
-
silently searches the cwd. No validate-then-repair layer can catch this
|
|
316
|
-
(commandcode's included); it would take strict schemas or key-similarity
|
|
317
|
-
heuristics, both with false-positive costs.
|
|
318
|
-
- Unrepairable input falls through to pi's stock validation error, which is
|
|
319
|
-
already model-readable (per-path issues plus the received arguments).
|
|
320
|
-
- If another extension also overrides a built-in tool, load order decides which
|
|
321
|
-
override wins — they don't compose.
|
|
322
|
-
- Phantom tool calls (a tool-use stop reason with zero tool-call blocks) are not
|
|
323
|
-
handled. On pi 0.80.6 that state is a clean terminal stop, not a recoverable
|
|
324
|
-
retry ([research.md Claim 6][r-claim6]), so normalizing it would be cosmetic;
|
|
325
|
-
it will be revisited only if a concrete stuck state is reproduced on a
|
|
326
|
-
provider.
|
|
327
|
-
|
|
328
|
-
[r-claim6]: docs/research.md#claim-6--stopreason-error-and-aborted-is-terminal-not-a-retry
|
|
207
|
+
Larger fuzz runs and replay instructions are in the
|
|
208
|
+
[operations guide](docs/operations.md#verification).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { DEFAULT_ENVELOPE_LIMITS, type EnvelopeLimits, type EnvelopeRecoveryResult, escapeJsonStringControlCharacters, isPlainObject, recoverEnvelope, truncatedEnvelopeChange, } from "./envelope.ts";
|
|
2
|
+
export { attachRepairNotes, formatRepairNotes, type RepairFeedback, RepairLifecycle, type RepairLifecycleOptions, stableSerialize, } from "./lifecycle.ts";
|
|
3
|
+
export { runRepairPipeline } from "./pipeline.ts";
|
|
4
|
+
export { type GrammarPolicyMode, type RepairPolicy, type RepairPolicyOverrides, type RepairPolicyProfile, resolveRepairPolicy, type UnknownGrammarTextPolicy, } from "./policy.ts";
|
|
5
|
+
export { type AliasPreprocessor, type FieldPreprocessor, type ObjectLocationSelector, type Preprocessor, preprocessInput, type StructuralPreprocessor, } from "./preprocess.ts";
|
|
6
|
+
export { type RepairResult, repairToolInput, type StructuralRepair, type ToolRepairConfig, unwrapMarkdownAutoLinks, } from "./repair-engine.ts";
|
|
7
|
+
export type { RepairChange, RepairObservation, RepairPipelineConfig, RepairPipelineLimits, RepairPipelineResult, RepairStage, } from "./types.ts";
|
|
8
|
+
//# sourceMappingURL=core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,iCAAiC,EACjC,aAAa,EACb,eAAe,EACf,uBAAuB,GACxB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,cAAc,EACnB,eAAe,EACf,KAAK,sBAAsB,EAC3B,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,mBAAmB,EACnB,KAAK,wBAAwB,GAC9B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EACjB,eAAe,EACf,KAAK,sBAAsB,GAC5B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,YAAY,EACjB,eAAe,EACf,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,WAAW,GACZ,MAAM,YAAY,CAAC"}
|
package/dist/src/core.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { DEFAULT_ENVELOPE_LIMITS, escapeJsonStringControlCharacters, isPlainObject, recoverEnvelope, truncatedEnvelopeChange, } from "./envelope.js";
|
|
2
|
+
export { attachRepairNotes, formatRepairNotes, RepairLifecycle, stableSerialize, } from "./lifecycle.js";
|
|
3
|
+
export { runRepairPipeline } from "./pipeline.js";
|
|
4
|
+
export { resolveRepairPolicy, } from "./policy.js";
|
|
5
|
+
export { preprocessInput, } from "./preprocess.js";
|
|
6
|
+
export { repairToolInput, unwrapMarkdownAutoLinks, } from "./repair-engine.js";
|
|
7
|
+
//# sourceMappingURL=core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EAGvB,iCAAiC,EACjC,aAAa,EACb,eAAe,EACf,uBAAuB,GACxB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EAEjB,eAAe,EAEf,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAKL,mBAAmB,GAEpB,MAAM,aAAa,CAAC;AACrB,OAAO,EAKL,eAAe,GAEhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAEL,eAAe,EAGf,uBAAuB,GACxB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RepairChange, RepairPipelineLimits } from "./types.ts";
|
|
2
|
+
export interface EnvelopeLimits {
|
|
3
|
+
maxInputBytes: number;
|
|
4
|
+
maxNestingDepth: number;
|
|
5
|
+
maxDecodeAttempts: number;
|
|
6
|
+
maxCandidates: number;
|
|
7
|
+
maxWorkMs: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const DEFAULT_ENVELOPE_LIMITS: EnvelopeLimits;
|
|
10
|
+
export interface EnvelopeRecoveryResult {
|
|
11
|
+
value: unknown;
|
|
12
|
+
changes: RepairChange[];
|
|
13
|
+
candidates: unknown[];
|
|
14
|
+
limited: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
17
|
+
/** Escape only literal JSON control characters that occur inside a string. */
|
|
18
|
+
export declare function escapeJsonStringControlCharacters(text: string): string;
|
|
19
|
+
export declare function recoverEnvelope(input: unknown, overrides?: RepairPipelineLimits): EnvelopeRecoveryResult;
|
|
20
|
+
export declare function truncatedEnvelopeChange(): RepairChange;
|
|
21
|
+
//# sourceMappingURL=envelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../../src/envelope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAErE,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,uBAAuB,EAAE,cAMrC,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,UAAU,EAAE,OAAO,EAAE,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMlC;AAiCD,8EAA8E;AAC9E,wBAAgB,iCAAiC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmCtE;AAgBD,wBAAgB,eAAe,CAC7B,KAAK,EAAE,OAAO,EACd,SAAS,GAAE,oBAAyB,GACnC,sBAAsB,CA4ExB;AAED,wBAAgB,uBAAuB,IAAI,YAAY,CAKtD"}
|