Skip to content

Commit a7e6689

Browse files
committed
feat: optional integration fallback for tool owners
1 parent 8b32118 commit a7e6689

16 files changed

Lines changed: 885 additions & 12 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ same repair pipeline to tools in your own extension.
88

99
No LLM calls. No network requests. No uploaded telemetry.
1010

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+
1125
## Why use it?
1226

1327
Models often understand the task but miss a detail of the tool contract:

docs/research.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ is protecting and where to re-verify it.
2323
`test/upstream-drift.test.ts` that execute the claims against the installed
2424
packages.
2525

26+
Claims 10–14 (optional integration) were added later with their own provenance:
27+
28+
- **Verification date:** 2026-07-18
29+
- **Source read:** the same pi clone (`v0.80.6-24-g0e6909f0`); line citations
30+
below refer to it.
31+
- **Empirical runs:** a live pi 0.80.10 install (npm-dist `cli.js` under
32+
Node 24) and the official `pi-linux-x64` release binary v0.80.10 (Bun
33+
compiled executable), each driven with a throwaway probe extension
34+
npm-installed into an isolated `$HOME/.pi/agent/npm` scope.
35+
2636
## Claims
2737

2838
### Claim 1 — Loop ordering: `prepareArguments` runs before validation, which runs before the `tool_call` event
@@ -202,6 +212,124 @@ queues value-free feedback, binds it at post-validation `tool_call`, then uses
202212
custom calls already have IDs and can be associated directly, without wrapping
203213
another extension's executor.
204214

215+
### Claim 10 — pi maintains one shared npm install project per scope, with flat sibling resolution
216+
217+
All `npm:`-installed extensions in a scope are dependencies of a single private
218+
npm project named `pi-extensions`, so their packages sit side by side in one
219+
flat `node_modules`. An end user who runs
220+
`pi install npm:@r3b1s/pi-repair-layer` therefore makes the package resolvable
221+
from every other npm-installed extension in that scope.
222+
223+
- `packages/coding-agent/src/core/package-manager.ts:1933-1944`
224+
`ensureNpmProject` writes `{ name: "pi-extensions", private: true }` into the
225+
install root's `package.json`.
226+
- `packages/coding-agent/src/core/package-manager.ts:1956-1964`
227+
`getNpmInstallRoot` returns `join(this.agentDir, "npm")` for the user scope
228+
(project scope roots under the project's config dir instead).
229+
- Live install (pi 0.80.10): `~/.pi/agent/npm/package.json` has
230+
`name: "pi-extensions"` with every npm-installed extension as a dependency,
231+
and `node_modules` holds them as flat siblings (bun-backed, `bun.lock`
232+
present).
233+
234+
**Why it matters:** this is the mechanism behind the shared-`node_modules`
235+
adoption path in the optional-integration recipe
236+
(`docs/tool-owner-integration.md`). It is observed managed-install layout, not
237+
a documented pi API guarantee — the recipe never *depends* on it (resolution
238+
either succeeds or falls back safely), but the adoption story does.
239+
240+
### Claim 11 — Missing-module error shapes: `MODULE_NOT_FOUND` (jiti/require) vs `ERR_MODULE_NOT_FOUND` (ESM import)
241+
242+
A dynamic import of an uninstalled package surfaces differently depending on
243+
the loader, but always with one of two `code` values and a message that names
244+
the requested module:
245+
246+
- jiti 2.7.0 require path (Node): plain `Error`, `code: "MODULE_NOT_FOUND"`,
247+
message `Cannot find module '<full specifier>'`.
248+
- Native Node ESM `import()` (Node 24): `Error`,
249+
`code: "ERR_MODULE_NOT_FOUND"`, message
250+
`Cannot find package '<package name>'`**it names the bare package, not
251+
the full subpath specifier**, so absence checks must match the package name
252+
(`@r3b1s/pi-repair-layer`), never the `/pi` subpath string.
253+
- Compiled Bun binary (pi-linux-x64 v0.80.10): Bun `ResolveMessage` (an
254+
`Error` subclass), `code: "ERR_MODULE_NOT_FOUND"` for `import()` and
255+
`"MODULE_NOT_FOUND"` for a scoped `require`, message
256+
`Cannot find module '<full specifier>' from '<importer path>'`.
257+
258+
**Why it matters:** the optional-integration recipe treats an import failure
259+
as "package absent" only when the code is one of these two values **and** the
260+
message names `@r3b1s/pi-repair-layer`; anything else rethrows so a broken
261+
install (a *transitive* module missing) is not silently misread as absent.
262+
All three observed shapes pass that discrimination.
263+
264+
### Claim 12 — Git installs and other scopes do not resolve the shared npm siblings
265+
266+
Git-installed extensions are cloned into `<agentDir>/git/<host>/<path>` and get
267+
their **own** dependency install inside the clone; they are not siblings of the
268+
shared npm root. User scope (`~/.pi/agent`) and project scope (`<cwd>/.pi`)
269+
likewise use separate install roots.
270+
271+
- `packages/coding-agent/src/core/package-manager.ts:1820-1846``installGit`
272+
clones into `getGitInstallPath(...)` and runs a dependency install inside the
273+
clone when it has a `package.json`.
274+
- `packages/coding-agent/src/core/package-manager.ts:2036-2046`
275+
`getGitInstallRoot` returns `join(this.agentDir, "git")` (user scope) or
276+
`join(this.cwd, CONFIG_DIR_NAME, "git")` (project scope).
277+
278+
**Why it matters:** a git-installed or cross-scope consumer cannot resolve an
279+
npm-installed `@r3b1s/pi-repair-layer` sibling, so the optional recipe falls
280+
back there even though the user "installed" the package. Documented as a hard
281+
caveat in the integration guide; `optionalDependencies` is the alternative for
282+
those consumers.
283+
284+
### Claim 13 — Bun-binary probe: the optional dynamic import falls back under the compiled binary, resolves under npm-dist pi
285+
286+
pi loads extensions through jiti (`moduleCache: false`; in the compiled binary
287+
additionally `virtualModules` + `tryNative: false`; under Node, `alias`):
288+
289+
- `packages/coding-agent/src/core/extensions/loader.ts:398-404` — the
290+
`createJiti` call and both option branches.
291+
292+
End-to-end probe (2026-07-18): a throwaway extension implementing the
293+
documented recipe, npm-installed into an isolated scope, run once with
294+
`@r3b1s/pi-repair-layer` npm-installed as a sibling and once without, under
295+
both pi distributions of v0.80.10:
296+
297+
| runtime | package | observed error (`name`/`code`) | branch taken |
298+
|---|---|---|---|
299+
| npm-dist (Node) | absent | `Error`/`ERR_MODULE_NOT_FOUND` | fallback, note emitted |
300+
| npm-dist (Node) | present || **adapted** |
301+
| compiled binary | absent | `ResolveMessage`/`ERR_MODULE_NOT_FOUND` | fallback, note emitted |
302+
| compiled binary | present | `ResolveMessage`/`ERR_MODULE_NOT_FOUND` | fallback, note emitted |
303+
304+
Under the compiled Bun binary, *static* imports of the sibling package do
305+
resolve (jiti's own resolver handles them — verified with a static-import
306+
probe extension in the same scope), but native dynamic `import()` and the
307+
scoped `require` both bypass jiti and hit Bun's embedded resolver, which does
308+
no filesystem `node_modules` resolution — even an absolute-path `import()` of
309+
the package's entry file loads but then dies on its transitive bare specifier
310+
(`typebox/value`). There is no consumer-accessible dynamic path through jiti's
311+
resolver as of this pi version.
312+
313+
**Why it matters:** the optional-integration pattern *activates* only under
314+
Node-based pi installs (npm/bun global install). Under the official compiled
315+
binary it degrades safely — the import failure has exactly the absent-package
316+
shape, so consumers fall back to their raw definition with the one-line note,
317+
even when the package is installed. A hard static dependency, by contrast,
318+
works under both distributions. Both facts are documented in the integration
319+
guide's caveats.
320+
321+
### Claim 14 — Unrecognized preprocessor kinds fall through `preprocessInput` untouched (local guarantee)
322+
323+
This claim is about this repo, not pi: `preprocessInput`
324+
(`src/preprocess.ts`) matches each configured entry against the known `kind`
325+
branches and simply skips entries it does not recognize — no mutation, no
326+
error, no claimed change — and the pipeline still schema-validates the final
327+
result. A consumer configured against a newer options shape therefore degrades
328+
to the recognized subset when running against an older installed version.
329+
Promoted to a spec-level compatibility guarantee by the
330+
`optional-integration-fallback` change and pinned by a unit test in
331+
`test/pipeline.test.ts`.
332+
205333
### Package/runtime assumptions
206334

207335
The published package targets Node 22+ and compiled ESM. `typebox` is a runtime
@@ -245,3 +373,18 @@ work through this list and update the citations/date above:
245373
messages, and handlers still compose in registration order.
246374
10. **Package/runtime:** run `pnpm run test:package`; re-check Node engines,
247375
peer/runtime dependencies, every `exports` target, and the pi extension path.
376+
11. **Shared npm root (Claim 10):** confirm `ensureNpmProject` still writes one
377+
`pi-extensions` project per scope and installs remain flat siblings —
378+
re-read `package-manager.ts` and inspect a live `~/.pi/agent/npm`.
379+
12. **Error shapes (Claim 11):** re-run a missing-module import under jiti, under
380+
native Node `import()`, and under the compiled binary; confirm the codes are
381+
still `MODULE_NOT_FOUND` / `ERR_MODULE_NOT_FOUND` and the message still names
382+
the requested package.
383+
13. **Git/scope boundaries (Claim 12):** confirm git installs still get their own
384+
clone-local dependency install and scopes still use separate install roots.
385+
14. **Bun-binary probe (Claim 13):** re-run the four-cell probe (npm-dist and
386+
compiled binary, package present and absent) with a throwaway recipe
387+
extension npm-installed into an isolated `$HOME`; update the outcome table —
388+
especially whether dynamic `import()` in the compiled binary has gained
389+
filesystem `node_modules` resolution, which would let optional consumers
390+
activate there.

0 commit comments

Comments
 (0)