Skip to content

Latest commit

 

History

History
195 lines (164 loc) · 8.04 KB

File metadata and controls

195 lines (164 loc) · 8.04 KB
name pi-tool-repair-integration
description Build or wrap tools in a pi coding-agent extension using @r3b1s/pi-repair-layer's adaptToolDefinition and the optional-dependency fallback pattern. Use when authoring a pi extension that registers tools, when adding argument repair (aliases, envelope recovery, schema-guided fixes) to an extension-owned tool, or when deciding whether pi-repair-layer should be a hard or optional dependency. Triggers on pi extensions, registerTool, prepareArguments, tool-call repair, or pi-repair-layer integration tasks.
license MIT
metadata
author version
r3b1s
1.0.0

pi Tool Repair Integration

Integrate @r3b1s/pi-repair-layer into a pi extension's own tools so malformed model arguments ({file_path: "/x"} instead of {path: "/x"}, stringified JSON, stray nulls) are repaired before pi validation — without forcing every downstream user to install the repair layer.

Verified baselines: package ^0.3.0, Node 22+, pi 0.80.6 (probed through 0.80.10).

The ownership boundary (read first)

pi runs prepareArguments → validation → tool_call → execute. Arguments that fail validation never reach tool_call, so repair must run in prepareArguments — and only the extension that owns a tool definition can install that hook. Installing pi-repair-layer repairs pi's built-in tools only; it never discovers or wraps tools registered by other extensions. Your extension must opt in per tool. Never attempt to wrap another extension's tools.

Step 1 — Choose the dependency posture

Posture When How
Optional (default for standalone extensions) The tool works fine unwrapped; repairs are an enhancement Fallback recipe below; package stays out of runtime deps
Hard dependency The tool relies on repair behavior (e.g. legacy-shape migration via preprocessors) pnpm add @r3b1s/pi-repair-layer; import statically

The optional posture turns "also install pi-repair-layer" into an end-user opt-in: pi installs all npm: extensions into one shared node_modules per scope, so a user who runs pi install npm:@r3b1s/pi-repair-layer makes it resolvable to every consenting npm-installed extension automatically.

Step 2 — Write the tool and repair options as pure data

Repair options must be plain data validated with a type-only import, so compilation never requires the package at runtime:

import type { PiToolOwnerAdapterOptions } from "@r3b1s/pi-repair-layer/pi";

const repairOptions = {
  policy: "adaptive",
  preprocessors: [
    {
      kind: "alias",
      selector: "/path",
      aliases: ["file_path"],
      accepts: "string",
    },
  ],
} satisfies PiToolOwnerAdapterOptions;

Configure only transforms you know are safe for your tool — see resources/preprocessor-catalog.md for every kind, selector semantics, and the policy profiles. The pipeline never guesses aliases, fuzzily renames keys, or deletes unknown fields; wrapped tools get bounded envelope recovery and schema-located repairs for free.

Step 3 — The optional-integration recipe

Copy resources/optional-extension-template.ts (a complete extension) and adapt names. The load-bearing core:

import type { adaptToolDefinition } from "@r3b1s/pi-repair-layer/pi";

async function loadRepairAdapter(): Promise<
  typeof adaptToolDefinition | undefined
> {
  try {
    const repair = await import("@r3b1s/pi-repair-layer/pi");
    return repair.adaptToolDefinition;
  } catch (error) {
    const code = (error as { code?: unknown } | null)?.code;
    const message = error instanceof Error ? error.message : String(error);
    const packageAbsent =
      (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") &&
      message.includes("@r3b1s/pi-repair-layer");
    if (!packageAbsent) throw error;
    return undefined;
  }
}

export default async function myExtension(pi: ExtensionAPI) {
  const adapt = await loadRepairAdapter();
  if (!adapt) {
    console.error(
      "[my-extension] @r3b1s/pi-repair-layer not found; my_tool running unwrapped",
    );
  }
  pi.registerTool(adapt ? adapt(definition, repairOptions) : definition);
}

Every detail matters — do not simplify these away:

  • Both error codes. MODULE_NOT_FOUND is jiti's require path; ERR_MODULE_NOT_FOUND is native ESM and the compiled pi binary.
  • The message must name @r3b1s/pi-repair-layer. A present-but-broken install throws the same codes naming a transitive module; swallowing it would silently disable repairs the user believes are active. Match the package name, not the /pi subpath — native ESM reports only Cannot find package '@r3b1s/pi-repair-layer'.
  • Rethrow anything else. Any other error is a real failure, not absence.
  • One stderr note on fallback. The branches differ in coercion behavior; a silent divergence cannot be diagnosed from a session transcript.
  • Identity fallback. Register the unmodified definition — never a partial wrapper.

Step 4 — Consumer package.json

{
  "devDependencies": {
    "@r3b1s/pi-repair-layer": "^0.3.0" // typecheck + local tests only
  },
  "peerDependencies": {
    "@r3b1s/pi-repair-layer": ">=0.3.0"
  },
  "peerDependenciesMeta": {
    "@r3b1s/pi-repair-layer": { "optional": true }
  }
}

Alternative: authors who want repairs whenever the environment allows can add "optionalDependencies": { "@r3b1s/pi-repair-layer": "^0.3.0" } — a failed optional install does not fail the extension install. This is also the path for consumers the shared-root story cannot reach (see caveats).

Step 5 — Test both branches

Work through resources/testing-checklist.md. Minimum bar: with the package absent, activation succeeds, the raw definition is registered, and the note is emitted; with it present, the adapter branch is taken silently and each configured repair produces valid arguments.

Hard caveats (state these to the user when relevant)

  1. Compiled pi binary never takes the adapter branch. Under the standalone (Bun-compiled) pi executable, dynamic import() cannot resolve npm-installed siblings, so the recipe falls back — safely, with the note — even when the package is installed. The optional pattern activates only under Node-based pi installs. A hard static dependency works under both.
  2. Scope and install source matter. Git-installed extensions get their own clone-local node_modules; project-scope and user-scope installs do not see each other's siblings. Those consumers fall back; offer optionalDependencies instead.
  3. Fallback mode is baseline pi, not "repairs minus notes." pi's native validation runs TypeBox Value.Convert first, which silently coerces some invalid input (null"null") instead of repairing or rejecting it. The tool owner must decide explicitly whether that is acceptable.
  4. No double-wrap. The installable pi-repair-layer extension only overrides pi's built-ins; a tool adapted by this recipe is wrapped exactly once in either branch.

Stability contract you may rely on

  • Subpaths (/pi, /core, /grammar) and the adaptToolDefinition(definition, options?) signature are stable for the current major (semver).
  • Absence detection semantics (the two codes + module-naming message) are part of the contract.
  • Unrecognized preprocessor kinds are ignored — never fatal, no mutation, results still schema-validated — so options written against a newer minor degrade gracefully on older installs.

Going further

  • Failing closed: the adapter throws UnrepairableToolInputError with a model-readable retry message by default; unrepairable: "passthrough" exists only for deliberate migrations.
  • Structured outcomes: pass onOutcome(result) for value-free metrics (rule IDs, stages, policy, fingerprint — never argument values).
  • <repair_note> feedback and the RepairLifecycle, plus the pure-core runRepairPipeline, are documented in the package's docs/tool-owner-integration.md.