@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
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
# Integrating repairs into extension-owned tools
|
|
2
|
+
|
|
3
|
+
This guide shows how a pi extension can apply pi-repair-layer's public repair
|
|
4
|
+
pipeline to tools that extension owns. The usual integration is one wrapper
|
|
5
|
+
around the tool definition. A lower-level core API is available when the tool
|
|
6
|
+
owner needs to control the surrounding lifecycle.
|
|
7
|
+
|
|
8
|
+
## Contents
|
|
9
|
+
|
|
10
|
+
- [The ownership boundary](#the-ownership-boundary)
|
|
11
|
+
- [Install the package](#install-the-package)
|
|
12
|
+
- [Recommended: wrap the tool definition](#recommended-wrap-the-tool-definition)
|
|
13
|
+
- [Optional integration](#optional-integration)
|
|
14
|
+
- [Configure only known-safe transforms](#configure-only-known-safe-transforms)
|
|
15
|
+
- [Existing `prepareArguments` hooks](#existing-preparearguments-hooks)
|
|
16
|
+
- [Optional: receive structured outcomes](#optional-receive-structured-outcomes)
|
|
17
|
+
- [Optional: attach `<repair_note>` feedback](#optional-attach-repair_note-feedback)
|
|
18
|
+
- [Lower level: call the pure core](#lower-level-call-the-pure-core)
|
|
19
|
+
- [Test the integration](#test-the-integration)
|
|
20
|
+
|
|
21
|
+
## The ownership boundary
|
|
22
|
+
|
|
23
|
+
pi validates tool arguments after calling that tool's `prepareArguments`.
|
|
24
|
+
Because arguments that fail validation never reach `tool_call`, repair must run
|
|
25
|
+
in `prepareArguments` to recover them safely.
|
|
26
|
+
|
|
27
|
+
Only the extension that owns a tool definition can reliably install that hook.
|
|
28
|
+
Installing pi-repair-layer repairs pi's built-in tools, but it does not discover,
|
|
29
|
+
replace, or wrap tools registered by other extensions. Those extensions must
|
|
30
|
+
explicitly use one of the APIs below.
|
|
31
|
+
|
|
32
|
+
## Install the package
|
|
33
|
+
|
|
34
|
+
Add pi-repair-layer as a dependency of the extension that owns the tool:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm add @r3b1s/pi-repair-layer
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The dependency does not have to be hard. A standalone extension can keep
|
|
41
|
+
pi-repair-layer out of its runtime dependencies entirely and degrade to its
|
|
42
|
+
raw tool definition when the package is absent — see
|
|
43
|
+
[Optional integration](#optional-integration) before committing your users to
|
|
44
|
+
an extra install.
|
|
45
|
+
|
|
46
|
+
The public APIs are compiled ESM with TypeScript declarations:
|
|
47
|
+
|
|
48
|
+
- `@r3b1s/pi-repair-layer/pi` provides the pi tool-definition adapter.
|
|
49
|
+
- `@r3b1s/pi-repair-layer/core` provides the pure pipeline, configuration
|
|
50
|
+
types, lifecycle helpers, and repair-note formatting.
|
|
51
|
+
- `@r3b1s/pi-repair-layer/grammar` provides pure leaked-grammar parsing and
|
|
52
|
+
recovery helpers.
|
|
53
|
+
|
|
54
|
+
Do not import files from `src/`; they are not part of the compatibility
|
|
55
|
+
contract. Node 22 or later is required. The verified pi integration baseline is
|
|
56
|
+
0.80.6.
|
|
57
|
+
|
|
58
|
+
## Recommended: wrap the tool definition
|
|
59
|
+
|
|
60
|
+
`adaptToolDefinition` returns a copy of the supplied definition with a repair
|
|
61
|
+
aware `prepareArguments`. It preserves the tool's schema and executor and
|
|
62
|
+
chains any `prepareArguments` already supplied by the owner.
|
|
63
|
+
|
|
64
|
+
Once wrapped, bounded envelope recovery and schema-located repairs such as
|
|
65
|
+
dropping an invalid optional `null` or parsing a JSON-stringified array are
|
|
66
|
+
automatic. Exact aliases, cleanup of values that already satisfy the schema,
|
|
67
|
+
and tool-specific shape changes require explicit preprocessors because only the
|
|
68
|
+
tool owner knows where those transformations are semantically safe.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
72
|
+
import { adaptToolDefinition } from "@r3b1s/pi-repair-layer/pi";
|
|
73
|
+
import { Type } from "typebox";
|
|
74
|
+
|
|
75
|
+
const parameters = Type.Object({
|
|
76
|
+
path: Type.String(),
|
|
77
|
+
labels: Type.Optional(Type.Array(Type.String())),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export default function myExtension(pi: ExtensionAPI) {
|
|
81
|
+
pi.registerTool(
|
|
82
|
+
adaptToolDefinition(
|
|
83
|
+
{
|
|
84
|
+
name: "inspect_asset",
|
|
85
|
+
label: "Inspect asset",
|
|
86
|
+
description: "Inspect an asset and optionally apply labels",
|
|
87
|
+
parameters,
|
|
88
|
+
async execute(_toolCallId, params) {
|
|
89
|
+
return {
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: "text",
|
|
93
|
+
text: `Inspecting ${params.path}`,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
details: undefined,
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
policy: "adaptive",
|
|
102
|
+
preprocessors: [
|
|
103
|
+
{
|
|
104
|
+
kind: "alias",
|
|
105
|
+
selector: "/path",
|
|
106
|
+
aliases: ["file_path", "filePath"],
|
|
107
|
+
accepts: "string",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
kind: "filesystem-path",
|
|
111
|
+
selector: "/path",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
kind: "string-or-array",
|
|
115
|
+
selector: "/labels",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
With this configuration, `{file_path: "/tmp/a"}` becomes
|
|
125
|
+
`{path: "/tmp/a"}`, a markdown auto-link in `path` is unwrapped, and a single
|
|
126
|
+
string in `labels` becomes a one-item array. Each mutation has a stable rule ID
|
|
127
|
+
and explanatory note, and the final result must pass the supplied schema.
|
|
128
|
+
|
|
129
|
+
The adapter fails closed by throwing `UnrepairableToolInputError` with a
|
|
130
|
+
model-readable retry message. `unrepairable: "passthrough"` is available for a
|
|
131
|
+
deliberate compatibility migration, but it can expose the input to pi's native
|
|
132
|
+
conversion and should not be the default.
|
|
133
|
+
|
|
134
|
+
## Optional integration
|
|
135
|
+
|
|
136
|
+
`adaptToolDefinition` is a pure decorator: it takes a `ToolDefinition` and
|
|
137
|
+
returns a `ToolDefinition`. That makes the identity function a complete
|
|
138
|
+
fallback — an extension can attempt to load the adapter at activation time and
|
|
139
|
+
register its unmodified definition when pi-repair-layer is not installed.
|
|
140
|
+
Repair support then becomes an end-user opt-in instead of an author-imposed
|
|
141
|
+
dependency.
|
|
142
|
+
|
|
143
|
+
### The recipe
|
|
144
|
+
|
|
145
|
+
Author repair options as pure data, import only types at compile time, and
|
|
146
|
+
attempt one dynamic import inside the (awaited) extension factory:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
150
|
+
import type {
|
|
151
|
+
adaptToolDefinition,
|
|
152
|
+
PiToolOwnerAdapterOptions,
|
|
153
|
+
} from "@r3b1s/pi-repair-layer/pi";
|
|
154
|
+
import { Type } from "typebox";
|
|
155
|
+
|
|
156
|
+
const parameters = Type.Object({ path: Type.String() });
|
|
157
|
+
|
|
158
|
+
const repairOptions = {
|
|
159
|
+
policy: "adaptive",
|
|
160
|
+
preprocessors: [
|
|
161
|
+
{
|
|
162
|
+
kind: "alias",
|
|
163
|
+
selector: "/path",
|
|
164
|
+
aliases: ["file_path"],
|
|
165
|
+
accepts: "string",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
} satisfies PiToolOwnerAdapterOptions;
|
|
169
|
+
|
|
170
|
+
const definition: ToolDefinition<typeof parameters> = {
|
|
171
|
+
name: "inspect_asset",
|
|
172
|
+
label: "Inspect asset",
|
|
173
|
+
description: "Inspect an asset",
|
|
174
|
+
parameters,
|
|
175
|
+
async execute(_toolCallId, params) {
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: "text", text: `Inspecting ${params.path}` }],
|
|
178
|
+
details: undefined,
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
async function loadRepairAdapter(): Promise<
|
|
184
|
+
typeof adaptToolDefinition | undefined
|
|
185
|
+
> {
|
|
186
|
+
try {
|
|
187
|
+
const repair = await import("@r3b1s/pi-repair-layer/pi");
|
|
188
|
+
return repair.adaptToolDefinition;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const code = (error as { code?: unknown } | null)?.code;
|
|
191
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
192
|
+
const packageAbsent =
|
|
193
|
+
(code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") &&
|
|
194
|
+
message.includes("@r3b1s/pi-repair-layer");
|
|
195
|
+
if (!packageAbsent) throw error;
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export default async function myExtension(pi: ExtensionAPI) {
|
|
201
|
+
const adapt = await loadRepairAdapter();
|
|
202
|
+
if (!adapt) {
|
|
203
|
+
console.error(
|
|
204
|
+
"[my-extension] @r3b1s/pi-repair-layer not found; inspect_asset running unwrapped",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
pi.registerTool(adapt ? adapt(definition, repairOptions) : definition);
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The details are load-bearing:
|
|
212
|
+
|
|
213
|
+
- **Discriminate before falling back.** A missing package surfaces as
|
|
214
|
+
`code: "MODULE_NOT_FOUND"` (jiti's require path) or
|
|
215
|
+
`code: "ERR_MODULE_NOT_FOUND"` (native ESM and the compiled pi binary). But
|
|
216
|
+
a *present-but-broken* install — pi-repair-layer resolvable while one of its
|
|
217
|
+
own transitive modules is not — throws the same codes naming the
|
|
218
|
+
*transitive* module. Requiring the message to name
|
|
219
|
+
`@r3b1s/pi-repair-layer` keeps a broken install loud instead of silently
|
|
220
|
+
running unwrapped while the user believes repairs are active. Match the
|
|
221
|
+
package name, not the `/pi` subpath: native ESM reports only
|
|
222
|
+
`Cannot find package '@r3b1s/pi-repair-layer'`.
|
|
223
|
+
- **Rethrow everything else.** Any other error is a real failure, not
|
|
224
|
+
absence.
|
|
225
|
+
- **Emit one stderr line when falling back.** The two branches differ in
|
|
226
|
+
behavior (see the caveats below); a silent divergence cannot be diagnosed
|
|
227
|
+
from a session transcript.
|
|
228
|
+
- **Keep repair options as pure data.** `satisfies PiToolOwnerAdapterOptions`
|
|
229
|
+
with a type-only import gives full typechecking with no runtime import, so
|
|
230
|
+
the compiled extension activates in installs without the package.
|
|
231
|
+
|
|
232
|
+
The runnable, tested copy of this recipe is
|
|
233
|
+
[`test/fixtures/optional-consumer.ts`](../test/fixtures/optional-consumer.ts);
|
|
234
|
+
the package smoke test exercises both branches in clean projects. Keep the
|
|
235
|
+
snippet above and that fixture in sync when editing either.
|
|
236
|
+
|
|
237
|
+
### Consumer `package.json` shape
|
|
238
|
+
|
|
239
|
+
```jsonc
|
|
240
|
+
{
|
|
241
|
+
"devDependencies": {
|
|
242
|
+
// for typechecking and local tests only; erased from the runtime story
|
|
243
|
+
"@r3b1s/pi-repair-layer": "^0.3.0"
|
|
244
|
+
},
|
|
245
|
+
"peerDependencies": {
|
|
246
|
+
"@r3b1s/pi-repair-layer": ">=0.3.0"
|
|
247
|
+
},
|
|
248
|
+
"peerDependenciesMeta": {
|
|
249
|
+
"@r3b1s/pi-repair-layer": { "optional": true }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`import type` and `satisfies` are erased at compile time, so the
|
|
255
|
+
`devDependencies` entry is all that is needed to build. The optional peer
|
|
256
|
+
declaration documents which versions the extension was written against
|
|
257
|
+
without forcing an install.
|
|
258
|
+
|
|
259
|
+
### Adoption paths
|
|
260
|
+
|
|
261
|
+
**End-user opt-in via pi's shared install root (the default story).** pi
|
|
262
|
+
installs every `npm:`-installed extension into one shared project per scope,
|
|
263
|
+
so all such extensions sit side by side in a flat `node_modules`. When an end
|
|
264
|
+
user runs:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
pi install npm:@r3b1s/pi-repair-layer
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
to get repairs for pi's built-in tools, the package also becomes resolvable to
|
|
271
|
+
every npm-installed extension in that scope — and consenting extensions using
|
|
272
|
+
this recipe light up automatically. Nothing is required from the extension
|
|
273
|
+
author beyond the recipe.
|
|
274
|
+
|
|
275
|
+
**Author-controlled `optionalDependencies`.** An author who wants repairs
|
|
276
|
+
whenever the environment can provide them can instead declare:
|
|
277
|
+
|
|
278
|
+
```jsonc
|
|
279
|
+
{
|
|
280
|
+
"optionalDependencies": {
|
|
281
|
+
"@r3b1s/pi-repair-layer": "^0.3.0"
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The package installs alongside the extension when possible, and a failed
|
|
287
|
+
optional install does not fail the extension's install. This is also the path
|
|
288
|
+
for consumers the shared-root story cannot reach (see the caveats).
|
|
289
|
+
|
|
290
|
+
### Caveats
|
|
291
|
+
|
|
292
|
+
- **Install source and scope matter.** The shared `node_modules` is per pi
|
|
293
|
+
scope, and only for `npm:` installs. Git-installed extensions get their own
|
|
294
|
+
clone-local dependency install, and project-scope installs do not resolve
|
|
295
|
+
user-scope siblings (or vice versa). Those consumers fall back even though
|
|
296
|
+
the user installed the package; `optionalDependencies` is the alternative
|
|
297
|
+
there. The layout is observed pi behavior (verified 0.80.6–0.80.10), not a
|
|
298
|
+
documented pi guarantee — see
|
|
299
|
+
[research Claim 10](research.md#claim-10--pi-maintains-one-shared-npm-install-project-per-scope-with-flat-sibling-resolution).
|
|
300
|
+
- **The compiled pi binary never takes the adapter branch today.** Under the
|
|
301
|
+
standalone (Bun-compiled) pi executable, dynamic `import()` cannot resolve
|
|
302
|
+
npm-installed siblings at all, so this recipe falls back — safely, with the
|
|
303
|
+
note — even when the package is installed. The optional pattern currently
|
|
304
|
+
activates only under Node-based pi installs (`npm install -g` /
|
|
305
|
+
`bun install -g`). A hard static dependency resolves under both
|
|
306
|
+
distributions. See
|
|
307
|
+
[research Claim 13](research.md#claim-13--bun-binary-probe-the-optional-dynamic-import-falls-back-under-the-compiled-binary-resolves-under-npm-dist-pi).
|
|
308
|
+
- **Fallback mode is baseline pi, not "repairs minus notes."** Without the
|
|
309
|
+
adapter, pi's native validation runs `Value.Convert` first, which silently
|
|
310
|
+
coerces some invalid input into valid-looking values (`null` → `"null"`)
|
|
311
|
+
instead of repairing or rejecting it. Decide explicitly whether that is
|
|
312
|
+
acceptable for your tool, and test both branches.
|
|
313
|
+
- **No double-wrap.** The installable pi-repair-layer extension only overrides
|
|
314
|
+
pi's built-in tools; it never discovers or wraps tools registered by other
|
|
315
|
+
extensions. A tool adapted by this recipe is wrapped exactly once in either
|
|
316
|
+
branch.
|
|
317
|
+
|
|
318
|
+
### Stability contract for optional consumers
|
|
319
|
+
|
|
320
|
+
Copies of this recipe keep working across versions because the surface it
|
|
321
|
+
touches is deliberately small and covered by the compatibility contract in
|
|
322
|
+
[Install the package](#install-the-package):
|
|
323
|
+
|
|
324
|
+
- The subpath names (`/pi`, `/core`, `/grammar`) and the
|
|
325
|
+
`adaptToolDefinition(definition, options?)` signature are stable for the
|
|
326
|
+
current major; documented exports follow semantic versioning.
|
|
327
|
+
- Absence detection semantics are part of the contract: a missing package
|
|
328
|
+
surfaces with `code` `MODULE_NOT_FOUND` or `ERR_MODULE_NOT_FOUND` and a
|
|
329
|
+
message naming the requested module.
|
|
330
|
+
- Unrecognized preprocessor `kind`s are ignored — never fatal, no mutation, no
|
|
331
|
+
claimed change — and results are still schema-validated. Repair options
|
|
332
|
+
written against a newer minor version therefore degrade to the recognized
|
|
333
|
+
subset when an older version is installed, instead of throwing at
|
|
334
|
+
configuration time.
|
|
335
|
+
|
|
336
|
+
## Configure only known-safe transforms
|
|
337
|
+
|
|
338
|
+
Preprocessors use JSON-Pointer-like selectors. `"/path"` selects a field,
|
|
339
|
+
`"/edits/*/oldText"` selects a field in every array item, and `""` selects the
|
|
340
|
+
root. Pointer escapes `~0` and `~1` represent `~` and `/` in field names.
|
|
341
|
+
|
|
342
|
+
Available preprocessors are:
|
|
343
|
+
|
|
344
|
+
| Kind | Intended use |
|
|
345
|
+
|---|---|
|
|
346
|
+
| `alias` | Move an explicitly listed alternative key to its canonical location. |
|
|
347
|
+
| `filesystem-path` | Unwrap a markdown auto-link in one path string. |
|
|
348
|
+
| `filesystem-path-array` | Apply path cleanup to string items in one array. |
|
|
349
|
+
| `string-or-array` | Wrap one configured string as a one-item array. |
|
|
350
|
+
| `scalar` | Convert a configured string/number/boolean scalar to the declared scalar type. |
|
|
351
|
+
| `anchor-bleed` | Strip model-gated `^`/`$` artifacts from one configured string. |
|
|
352
|
+
| `grammar-tokens` | Strip model-gated argument tokens at configured keys/values. |
|
|
353
|
+
| `structural` | Apply an owner-supplied shape transformation with a stable rule ID and note. |
|
|
354
|
+
|
|
355
|
+
Alias values can be guarded with `accepts: "string"`, `"number"`,
|
|
356
|
+
`"boolean"`, `"array"`, or `"object"`. By default, an existing canonical
|
|
357
|
+
value wins. Set `emptyEquivalentToMissing: true` only when an empty canonical
|
|
358
|
+
value is invalid or genuinely means “not supplied.”
|
|
359
|
+
|
|
360
|
+
Model-specific transforms require both a matching `modelId` and configured
|
|
361
|
+
`modelFamilies`:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
{
|
|
365
|
+
policy: "adaptive",
|
|
366
|
+
modelId: "provider/model-id",
|
|
367
|
+
preprocessors: [{
|
|
368
|
+
kind: "anchor-bleed",
|
|
369
|
+
selector: "/path",
|
|
370
|
+
modelFamilies: [/kimi-k2/i, /glm/i],
|
|
371
|
+
}],
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Adapter options are captured when the definition is wrapped. If the tool must
|
|
376
|
+
follow model changes during a session, either re-register it with the new model
|
|
377
|
+
ID or use the pure-core pattern below from an owner-managed `prepareArguments`
|
|
378
|
+
that reads current model state for each call.
|
|
379
|
+
|
|
380
|
+
Do not apply path cleanup to arbitrary content or anchor cleanup to regex
|
|
381
|
+
fields. The pipeline deliberately does not guess aliases, fuzzily rename keys,
|
|
382
|
+
or delete unknown fields.
|
|
383
|
+
|
|
384
|
+
### Policy profiles
|
|
385
|
+
|
|
386
|
+
- `conservative` permits bounded lossless envelope recovery, exact configured
|
|
387
|
+
preprocessing, and schema-guided repair. It observes model-gated valid-value
|
|
388
|
+
artifacts without mutating them.
|
|
389
|
+
- `adaptive` additionally permits schema-validated truncated-object completion
|
|
390
|
+
and configured model-gated value cleanup. This is the default.
|
|
391
|
+
- `recover` has the same tool-argument behavior as adaptive and additionally
|
|
392
|
+
enables gated assistant-text grammar promotion when the installable extension
|
|
393
|
+
is handling `message_end`.
|
|
394
|
+
|
|
395
|
+
The adapter itself does not register grammar hooks or turn assistant text into
|
|
396
|
+
tool calls.
|
|
397
|
+
|
|
398
|
+
## Existing `prepareArguments` hooks
|
|
399
|
+
|
|
400
|
+
If the definition already has `prepareArguments`, keep it on the definition.
|
|
401
|
+
The adapter calls it after envelope recovery and before configured
|
|
402
|
+
preprocessors and schema repair:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
pi.registerTool(
|
|
406
|
+
adaptToolDefinition({
|
|
407
|
+
...definition,
|
|
408
|
+
prepareArguments(input) {
|
|
409
|
+
return migrateMyExtensionVersion(input);
|
|
410
|
+
},
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The owner shim should perform only compatibility work belonging to that tool.
|
|
416
|
+
If it throws, the pipeline preserves the recovered envelope and continues to a
|
|
417
|
+
fail-closed validity decision.
|
|
418
|
+
|
|
419
|
+
## Optional: receive structured outcomes
|
|
420
|
+
|
|
421
|
+
Use `onOutcome` for metrics or owner-managed feedback. It runs once with the
|
|
422
|
+
same result returned by the pipeline:
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
adaptToolDefinition(definition, {
|
|
426
|
+
onOutcome(result) {
|
|
427
|
+
if (result.outcome === "repaired") {
|
|
428
|
+
recordValueFreeMetric({
|
|
429
|
+
tool: definition.name,
|
|
430
|
+
policy: result.policy,
|
|
431
|
+
rules: result.changes.map((change) => change.ruleId),
|
|
432
|
+
stages: result.changes.map((change) => change.stage),
|
|
433
|
+
fingerprint: result.fingerprint,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
`result.args` contains the actual arguments. Do not write it to telemetry or
|
|
441
|
+
session metadata. Rule IDs, stages, policy, outcome, and non-value failure
|
|
442
|
+
fingerprints are suitable for value-free reporting. Callback errors are
|
|
443
|
+
isolated and do not alter the repair verdict.
|
|
444
|
+
|
|
445
|
+
## Optional: attach `<repair_note>` feedback
|
|
446
|
+
|
|
447
|
+
The adapter repairs arguments, but it cannot attach feedback by itself because
|
|
448
|
+
`prepareArguments` runs before pi creates or exposes the validated call through
|
|
449
|
+
`tool_call`. Tool owners that want model-facing notes can use the exported
|
|
450
|
+
bounded `RepairLifecycle` to bridge that gap:
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
import {
|
|
454
|
+
attachRepairNotes,
|
|
455
|
+
RepairLifecycle,
|
|
456
|
+
} from "@r3b1s/pi-repair-layer/core";
|
|
457
|
+
import { adaptToolDefinition } from "@r3b1s/pi-repair-layer/pi";
|
|
458
|
+
|
|
459
|
+
const lifecycle = new RepairLifecycle();
|
|
460
|
+
const toolName = definition.name;
|
|
461
|
+
|
|
462
|
+
pi.registerTool(
|
|
463
|
+
adaptToolDefinition(definition, {
|
|
464
|
+
policy: "adaptive",
|
|
465
|
+
preprocessors,
|
|
466
|
+
onOutcome(result) {
|
|
467
|
+
if (result.outcome !== "repaired") return;
|
|
468
|
+
lifecycle.enqueue(toolName, result.args, {
|
|
469
|
+
rules: result.changes.map((change) => change.ruleId),
|
|
470
|
+
notes: result.changes.map((change) => change.note),
|
|
471
|
+
stages: [...new Set(result.changes.map((change) => change.stage))],
|
|
472
|
+
profile: result.policy,
|
|
473
|
+
outcome: "repaired",
|
|
474
|
+
fingerprint: result.fingerprint,
|
|
475
|
+
});
|
|
476
|
+
},
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
pi.on("tool_call", async (event) => {
|
|
481
|
+
lifecycle.correlate(event.toolName, event.input, event.toolCallId);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
pi.on("tool_result", async (event) => {
|
|
485
|
+
const feedback = lifecycle.take(event.toolCallId);
|
|
486
|
+
if (!feedback || feedback.notes.length === 0) return undefined;
|
|
487
|
+
return {
|
|
488
|
+
content: attachRepairNotes(event.content, feedback.notes),
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
pi.on("session_shutdown", async () => {
|
|
493
|
+
lifecycle.clear();
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
The lifecycle uses stable argument serialization, FIFO matching for identical
|
|
498
|
+
concurrent calls, bounded queues, and a TTL. `attachRepairNotes` preserves other
|
|
499
|
+
content and avoids inserting an identical tagged note twice. A custom TUI
|
|
500
|
+
indicator remains the owning extension's responsibility.
|
|
501
|
+
|
|
502
|
+
## Lower level: call the pure core
|
|
503
|
+
|
|
504
|
+
Use `runRepairPipeline` when the adapter does not fit the tool framework or when
|
|
505
|
+
repair is needed outside a pi session:
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
import { runRepairPipeline } from "@r3b1s/pi-repair-layer/core";
|
|
509
|
+
|
|
510
|
+
const result = runRepairPipeline({
|
|
511
|
+
input: rawArguments,
|
|
512
|
+
config: {
|
|
513
|
+
toolName: "inspect_asset",
|
|
514
|
+
schema: parameters,
|
|
515
|
+
policy: "adaptive",
|
|
516
|
+
preprocessors,
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
if (result.outcome === "unrepairable") {
|
|
521
|
+
throw new Error(result.retryMessage);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const validatedArguments = result.args;
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
The core registers no pi events and performs no filesystem, UI, telemetry,
|
|
528
|
+
network, or persistence work. It does not mutate caller-owned input. A
|
|
529
|
+
`repaired` outcome always means the final arguments passed the supplied schema;
|
|
530
|
+
an `unrepairable` outcome returns the original input and no claimed changes.
|
|
531
|
+
|
|
532
|
+
## Test the integration
|
|
533
|
+
|
|
534
|
+
At minimum, an owning extension should test:
|
|
535
|
+
|
|
536
|
+
1. Strictly valid input remains unchanged.
|
|
537
|
+
2. Every configured alias and selector repair produces valid arguments.
|
|
538
|
+
3. Unconfigured content and similar-but-unknown keys remain unchanged.
|
|
539
|
+
4. Unrepairable input throws before the executor runs.
|
|
540
|
+
5. An existing owner `prepareArguments` is still called.
|
|
541
|
+
6. If result feedback is enabled, concurrent identical calls receive one note
|
|
542
|
+
each and stale notes do not leak to later calls.
|
|
543
|
+
7. Telemetry and persisted entries contain no argument values or note text.
|
|
544
|
+
|
|
545
|
+
Optional consumers (the [Optional integration](#optional-integration) recipe)
|
|
546
|
+
should additionally test both branches:
|
|
547
|
+
|
|
548
|
+
8. With the package absent, activation still succeeds: the raw definition is
|
|
549
|
+
registered and the one-line fallback note is emitted.
|
|
550
|
+
9. Strictly valid input behaves identically in both branches; for invalid
|
|
551
|
+
input, the fallback branch exposes pi's native coercion — confirm that is
|
|
552
|
+
acceptable for your tool.
|
|
553
|
+
|
|
554
|
+
For a working compile-time example, see
|
|
555
|
+
[`test/fixtures/public-consumer.ts`](../test/fixtures/public-consumer.ts). The
|
|
556
|
+
package smoke test installs the packed tarball into a clean project and imports
|
|
557
|
+
every documented subpath.
|
package/package.json
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@r3b1s/pi-repair-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Tool-input repair layer for the pi coding agent: validate-then-repair for built-in tool calls, ported from the behavior of commandcode's repair layer",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/r3b1s/pi-repair-layer.git"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"dist",
|
|
11
|
+
"docs",
|
|
12
12
|
"README.md",
|
|
13
13
|
"LICENSE"
|
|
14
14
|
],
|
|
15
15
|
"type": "module",
|
|
16
|
-
"main": "
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./core": {
|
|
24
|
+
"types": "./dist/src/core.d.ts",
|
|
25
|
+
"import": "./dist/src/core.js"
|
|
26
|
+
},
|
|
27
|
+
"./pi": {
|
|
28
|
+
"types": "./dist/src/pi.d.ts",
|
|
29
|
+
"import": "./dist/src/pi.js"
|
|
30
|
+
},
|
|
31
|
+
"./grammar": {
|
|
32
|
+
"types": "./dist/src/grammar.d.ts",
|
|
33
|
+
"import": "./dist/src/grammar.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
17
36
|
"license": "MIT",
|
|
18
37
|
"publishConfig": {
|
|
19
38
|
"access": "public"
|
|
@@ -27,7 +46,7 @@
|
|
|
27
46
|
],
|
|
28
47
|
"pi": {
|
|
29
48
|
"extensions": [
|
|
30
|
-
"./
|
|
49
|
+
"./dist/index.js"
|
|
31
50
|
]
|
|
32
51
|
},
|
|
33
52
|
"dependencies": {
|
|
@@ -37,6 +56,7 @@
|
|
|
37
56
|
"@biomejs/biome": "2.4.8",
|
|
38
57
|
"@earendil-works/pi-ai": "^0.80.6",
|
|
39
58
|
"@earendil-works/pi-coding-agent": "0.80.6",
|
|
59
|
+
"@earendil-works/pi-tui": "0.80.6",
|
|
40
60
|
"@types/node": "^24.0.0",
|
|
41
61
|
"eslint": "^9.25.1",
|
|
42
62
|
"typescript": "^5.9.3",
|
|
@@ -44,17 +64,22 @@
|
|
|
44
64
|
"vitest": "^4.1.10"
|
|
45
65
|
},
|
|
46
66
|
"peerDependencies": {
|
|
47
|
-
"@earendil-works/pi-coding-agent": "*"
|
|
67
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
68
|
+
"@earendil-works/pi-tui": "*"
|
|
48
69
|
},
|
|
49
70
|
"engines": {
|
|
50
71
|
"node": ">=22"
|
|
51
72
|
},
|
|
52
73
|
"scripts": {
|
|
53
74
|
"check": "tsc --noEmit",
|
|
75
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
54
76
|
"lint": "biome check . && eslint src/ test/ index.ts",
|
|
55
77
|
"test": "vitest run",
|
|
78
|
+
"test:fuzz": "vitest run test/envelope-fuzz.test.ts",
|
|
79
|
+
"test:fuzz:large": "PI_REPAIR_FUZZ_CASES=10000 vitest run test/envelope-fuzz.test.ts",
|
|
56
80
|
"test:watch": "vitest",
|
|
57
81
|
"format": "biome check --write .",
|
|
82
|
+
"test:package": "node scripts/package-smoke.mjs",
|
|
58
83
|
"clean": "rm -rf dist node_modules coverage"
|
|
59
84
|
}
|
|
60
85
|
}
|