feat(ai,agent,coding-agent): validate tool arguments with pi-schema, drop TypeBox

pi-schema: opt-in coercion, compile(schema, { coerce: true }) with coerce() and
parse(), the superset of TypeBox's Value.Convert rules and pi-ai's old coercion;
unresolvedRefs option; url and json-pointer-uri-fragment formats.
pi-ai: Tool takes any JSON Schema; validation through pi-schema; Type, Static
and TSchema are no longer exported (breaking). pi-agent-core and pi-durable:
tools on pi-schema, no TypeBox dependency. Coding agent: extensions keep Type,
Static, TSchema and StringEnum through an extension view of pi-ai; built-in
tools stay on TypeBox.
This commit is contained in:
Mario Zechner 2026-11-01 10:08:10 +00:00
parent 2c8d1feb14
commit 6536f80129
139 changed files with 5115 additions and 1305 deletions

View file

@ -12,6 +12,8 @@
### Changed
- Tool argument validation runs on `@earendil-works/pi-schema`. TypeBox features outside JSON Schema (`Type.Refine()`, `Type.BigInt()`, `Type.Undefined()`, codecs, `Format.Set()` formats) no longer apply to tool arguments; see [Tool parameter schemas and TypeBox](docs/extensions.md#tool-parameter-schemas-and-typebox). Validation failures the model sees list pi-schema's messages (`- path: missing`, `- limit: expected number, got string`) instead of TypeBox's (`must have required properties path`, `must be number`); which arguments are accepted and how they are coerced is unchanged for extension and built-in tools.
- Extensions keep importing `Type`, `Static` and `TSchema` from `@earendil-works/pi-ai`: Pi maps that import to pi-ai's compat entry plus the TypeBox it bundles, now that pi-ai itself no longer re-exports TypeBox, and its `StringEnum` there stays a TypeBox `TUnsafe<T>`. The built-in tool factories (`createReadTool()` and the others) return `TypeBoxAgentTool<typeof schema>`, which keeps `params` typed for TypeBox.
- Tightened system prompt
- `pi update` for installs from the pi.dev installer no longer passes `--min-release-age=0` to `npm ci`. The release lockfile pins every package, so the flag had no effect, and security scanners that block it stopped the update ([#11262](https://github.com/earendil-works/pi/issues/11262))
- `/model` and `/scoped-models` now show a model's name next to its ID when the name adds words the ID lacks, such as a renamed `models.json` override ([#5916](https://github.com/earendil-works/pi/issues/5916), [#11347](https://github.com/earendil-works/pi/pull/11347))

View file

@ -159,7 +159,7 @@ Settings also accept built-in extensions as `builtin:<name>` entries; see [Setti
| `@earendil-works/pi-coding-agent` | Extension types (`ExtensionAPI`, `ExtensionContext`, events) and helpers |
| `@earendil-works/pi-agent-core` | Agent message and tool types |
| `typebox`, `typebox/compile`, `typebox/value` | Schema definitions for tool parameters |
| `@earendil-works/pi-ai` | AI utilities (`StringEnum` for string enums, transcript helpers, model types) |
| `@earendil-works/pi-ai` | AI utilities (`StringEnum` for string enums, transcript helpers, model types). Inside Pi it also exports TypeBox's `Type`, `Static` and `TSchema`, which the published package no longer re-exports; import them from `typebox` in new code |
| `@earendil-works/pi-ai/compat`, `/models`, `/oauth`, `/providers/all` | Provider construction, built-in APIs, and OAuth helpers |
| `@earendil-works/pi-tui` | TUI components for custom rendering |
@ -169,6 +169,18 @@ npm dependencies work too. Add a `package.json` next to your extension (or in a
For distributed Pi packages installed with `pi install` (npm or git), runtime deps must be in `dependencies`; `devDependencies` are not installed. Do not list the host-provided packages above in `dependencies`. See [Declare dependencies](packages.md#declare-dependencies).
### Tool parameter schemas and TypeBox
Pi validates tool arguments as JSON Schema with `@earendil-works/pi-schema`, whatever built the schema; TypeBox schemas are JSON Schema and work unchanged. Before validation, loosely typed arguments are coerced: numeric strings to numbers, `"true"` to `true`, a single value to a one-item array, `null` in an optional property dropped. TypeBox features outside JSON Schema do not apply to tool arguments:
- `Type.Refine()` refinements are not run. Check such constraints in `execute()`.
- `Type.BigInt()`, `Type.Undefined()` and `Type.Void()` reject every argument, since JSON has no such values. Codecs (`Type.Codec()`) do not decode: `execute()` receives the JSON value.
- Formats registered with TypeBox's `Format.Set()` are not checked. The standard formats are, plus `url` and `json-pointer-uri-fragment`.
- Property names are matched as names, not as regular expressions, and `additionalProperties` applies only to properties the object does not declare.
- A `$ref` that names nothing rejects only arguments at its place.
Validation errors the model sees use pi-schema's messages, such as `- path: missing` or `- limit: expected number, got string`.
Node.js built-ins (`node:fs`, `node:path`, etc.) are also available.
## Writing an Extension

View file

@ -0,0 +1,180 @@
// Type-checks the example extensions against the modules extensions see at run time (core/extensions/virtual-modules.ts):
// "@earendil-works/pi-ai" is the extension view, which adds TypeBox's Type, Static and TSchema to the compat entry (the
// view imports "@earendil-works/pi-ai/compat", so that path stays the package's own).
// The other paths repeat the root tsconfig.json; keep them in sync.
// Also the test that imports the subagent example: it type-checks only against the extension view.
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"paths": {
"@earendil-works/chord": [
"../../../packages/chord/src/index.ts"
],
"@earendil-works/chord/context": [
"../../../packages/chord/src/context/index.ts"
],
"@earendil-works/chord/delta": [
"../../../packages/chord/src/delta/index.ts"
],
"@earendil-works/chord/next": [
"../../../packages/chord/src/next/index.ts"
],
"@earendil-works/chord/next/plugins": [
"../../../packages/chord/src/next-node/plugins/index.ts"
],
"@earendil-works/chord/next/plugins/runtime": [
"../../../packages/chord/src/next-node/plugins/runtime.ts"
],
"@test/contracts/*": [
"../../../packages/chord/test/next-node/fixtures/plugins/contracts/*.ts"
],
"@example/contracts/*": [
"../../../packages/chord/test/next/examples/35-plugins/plugins/contracts/*.ts"
],
"@example/weather/*": [
"../../../packages/chord/test/next/examples/36-plugin-failures/plugins/weather/*.ts"
],
"@example/prices/*": [
"../../../packages/chord/test/next/examples/36-plugin-failures/plugins/prices/*.ts"
],
"@example/shapes/*": [
"../../../packages/chord/test/next/examples/37-plugin-imports/plugins/shapes/*.ts"
],
"@example/shared/*": [
"../../../packages/chord/test/next/examples/38-watch-and-rollout/plugins/contracts/*.ts"
],
"@earendil-works/chord/next/http": [
"../../../packages/chord/src/next-node/http/index.ts"
],
"@earendil-works/chord/next/workers": [
"../../../packages/chord/src/next-node/workers/index.ts"
],
"@earendil-works/chord/node": [
"../../../packages/chord/src/node.ts"
],
"@earendil-works/chord/output": [
"../../../packages/chord/src/output.ts"
],
"@earendil-works/chord/telemetry-sqlite": [
"../../../packages/chord/src/telemetry-sqlite/index.ts"
],
"@earendil-works/chord/sqlite": [
"../../../packages/chord/src/sqlite/index.ts"
],
"@earendil-works/chord/sqlite/node": [
"../../../packages/chord/src/sqlite/node.ts"
],
"@earendil-works/chord/sqlite/cloudflare": [
"../../../packages/chord/src/sqlite/cloudflare.ts"
],
"@earendil-works/pi-telemetry": [
"../../../packages/telemetry/src/index.ts"
],
"@earendil-works/pi-telemetry/testing": [
"../../../packages/telemetry/src/testing/index.ts"
],
"@earendil-works/pi-telemetry/render": [
"../../../packages/telemetry/src/render.ts"
],
"@earendil-works/pi-codemode": [
"../../../packages/codemode/src/index.ts"
],
"@earendil-works/pi-codemode/cloudflare": [
"../../../packages/codemode/src/cloudflare.ts"
],
"@earendil-works/pi-codemode/declarations": [
"../../../packages/codemode/src/declarations.ts"
],
"@earendil-works/pi-codemode/portable": [
"../../../packages/codemode/src/portable.ts"
],
"@earendil-works/pi-codemode/source": [
"../../../packages/codemode/src/source.ts"
],
"@earendil-works/pi-codemode/worker": [
"../../../packages/codemode/src/runtime/worker.ts"
],
"@earendil-works/pi-schema": [
"../../../packages/schema/src/index.ts"
],
"@earendil-works/pi-schema/formats": [
"../../../packages/schema/src/formats/index.ts"
],
"@earendil-works/pi-mcp": [
"../../../packages/mcp/src/index.ts"
],
"@earendil-works/pi-mcp/oauth": [
"../../../packages/mcp/src/oauth/index.ts"
],
"@earendil-works/pi-mcp/testing": [
"../../../packages/mcp/src/testing/index.ts"
],
"@earendil-works/pi-ai": [
"../../../packages/coding-agent/src/core/extensions/pi-ai-extension-view.ts"
],
"@earendil-works/pi-ai/oauth": [
"../../../packages/ai/src/oauth.ts"
],
"@earendil-works/pi-ai/*": [
"../../../packages/ai/src/*.ts",
"../../../packages/ai/src/providers/*.ts"
],
"@earendil-works/pi-ai/dist/*": [
"../../../packages/ai/src/*"
],
"@earendil-works/pi-durable": [
"../../../packages/durable/src/index.ts"
],
"@earendil-works/pi-durable/testing": [
"../../../packages/durable/src/testing/index.ts"
],
"@earendil-works/pi-durable/*": [
"../../../packages/durable/src/*.ts",
"../../../packages/durable/src/*/index.ts",
"../../../packages/durable/src/*"
],
"@earendil-works/pi-env": [
"../../../packages/env/src/index.ts"
],
"@earendil-works/pi-agent-core": [
"../../../packages/agent/src/index.ts"
],
"@earendil-works/pi-coding-agent": [
"../../../packages/coding-agent/src/index.ts"
],
"@earendil-works/pi-coding-agent/hooks": [
"../../../packages/coding-agent/src/core/hooks/index.ts"
],
"@earendil-works/pi-coding-agent/*": [
"../../../packages/coding-agent/src/*"
],
"typebox": [
"../../../node_modules/typebox"
],
"@earendil-works/pi-tui": [
"../../../packages/tui/src/index.ts"
],
"@earendil-works/pi-tui/*": [
"../../../packages/tui/src/*"
],
"@earendil-works/pi-agent-old": [
"../../../packages/agent-old/src/index.ts"
],
"@earendil-works/pi-agent-old/*": [
"../../../packages/agent-old/src/*"
],
"@mariozechner/pi-ai": [
"../../../packages/coding-agent/src/core/extensions/pi-ai-extension-view.ts"
]
}
},
"include": [
"./extensions/**/*",
"../../*/src/**/*.d.ts",
"../test/suite/regressions/8261-subagent-project-trust.test.ts"
],
"exclude": [
"**/node_modules/**",
"./extensions/gondolin/**"
]
}

View file

@ -391,7 +391,7 @@
"license": "MIT",
"dependencies": {
"@earendil-works/pi-ai": "1.4.0",
"typebox": "1.3.27"
"@earendil-works/pi-schema": "1.4.0"
},
"engines": {
"node": ">=22.19.0"
@ -404,14 +404,14 @@
"dependencies": {
"@anthropic-ai/sdk": "0.129.0",
"@aws-sdk/client-bedrock-runtime": "3.1127.0",
"@earendil-works/pi-schema": "1.4.0",
"@earendil-works/pi-telemetry": "1.4.0",
"@google/genai": "2.21.0",
"@smithy/node-http-handler": "4.12.1",
"http-proxy-agent": "9.1.0",
"https-proxy-agent": "9.1.0",
"openai": "7.19.0",
"partial-json": "0.1.7",
"typebox": "1.3.27"
"partial-json": "0.1.7"
},
"bin": {
"pi-ai": "dist/cli.js"
@ -537,6 +537,14 @@
"node": ">=22.19.0"
}
},
"node_modules/@earendil-works/pi-schema": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-schema/-/pi-schema-1.4.0.tgz",
"license": "MIT",
"engines": {
"node": ">=22.19.0"
}
},
"node_modules/@earendil-works/pi-telemetry": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-1.4.0.tgz",

View file

@ -255,13 +255,6 @@
"type": "array",
"items": {
"type": "object",
"required": [
"inputTokensAbove",
"input",
"output",
"cacheRead",
"cacheWrite"
],
"properties": {
"inputTokensAbove": {
"type": "number",
@ -283,7 +276,14 @@
"type": "number",
"description": "Cache-write cost in USD per million tokens."
}
}
},
"required": [
"inputTokensAbove",
"input",
"output",
"cacheRead",
"cacheWrite"
]
},
"description": "Request-wide pricing tiers. The highest matching input threshold applies to the full request."
}
@ -383,12 +383,6 @@
"$defs": {
"ModelCost": {
"type": "object",
"required": [
"input",
"output",
"cacheRead",
"cacheWrite"
],
"properties": {
"input": {
"type": "number",
@ -410,13 +404,6 @@
"type": "array",
"items": {
"type": "object",
"required": [
"inputTokensAbove",
"input",
"output",
"cacheRead",
"cacheWrite"
],
"properties": {
"inputTokensAbove": {
"type": "number",
@ -438,11 +425,24 @@
"type": "number",
"description": "Cache-write cost in USD per million tokens."
}
}
},
"required": [
"inputTokensAbove",
"input",
"output",
"cacheRead",
"cacheWrite"
]
},
"description": "Request-wide pricing tiers. The highest matching input threshold applies to the full request."
}
}
},
"required": [
"input",
"output",
"cacheRead",
"cacheWrite"
]
},
"ModelInputLimits": {
"type": "object",
@ -628,9 +628,6 @@
},
{
"type": "object",
"required": [
"$var"
],
"properties": {
"$var": {
"anyOf": [
@ -651,7 +648,10 @@
"omitWhenOff": {
"type": "boolean"
}
}
},
"required": [
"$var"
]
}
]
}
@ -677,9 +677,6 @@
},
{
"type": "object",
"required": [
"$var"
],
"properties": {
"$var": {
"anyOf": [
@ -700,7 +697,10 @@
"omitWhenOff": {
"type": "boolean"
}
}
},
"required": [
"$var"
]
}
]
}
@ -1085,11 +1085,6 @@
"type": "array",
"items": {
"type": "object",
"required": [
"provider",
"model",
"cost"
],
"properties": {
"provider": {
"type": "string",
@ -1103,6 +1098,11 @@
"$ref": "#/$defs/ModelCost"
}
},
"required": [
"provider",
"model",
"cost"
],
"description": "An Anthropic server-side refusal fallback model with local pricing metadata."
},
"maxItems": 3,

View file

@ -88,10 +88,11 @@ function getAliases(): Record<string, string> {
const piCodingAgentEntry = packageIndex;
const piAgentCoreEntry = resolveWorkspaceOrImport("agent/dist/index.js", "@earendil-works/pi-agent-core");
const piTuiEntry = resolveWorkspaceOrImport("tui/dist/index.js", "@earendil-works/pi-tui");
// Extensions resolve the pi-ai root to the compat entrypoint (a strict
// superset of the core entrypoint): existing extensions using the old
// global API keep working at runtime until compat is removed.
const piAiCompatEntry = resolveWorkspaceOrImport("ai/dist/compat.js", "@earendil-works/pi-ai/compat");
// Extensions resolve the pi-ai root to the extension view: the compat
// entrypoint (a strict superset of the core entrypoint) plus TypeBox's Type,
// Static and TSchema, so extensions using the old global API and pi-ai's
// former TypeBox re-exports keep working.
const piAiCompatEntry = path.resolve(__dirname, "pi-ai-extension-view.js");
const piAiModelsEntry = resolveWorkspaceOrImport("ai/dist/models.js", "@earendil-works/pi-ai/models");
const piAiOauthEntry = resolveWorkspaceOrImport("ai/dist/oauth.js", "@earendil-works/pi-ai/oauth");
const piAiProvidersEntry = resolveWorkspaceOrImport(

View file

@ -0,0 +1,28 @@
/**
* The `@earendil-works/pi-ai` module extensions import: pi-ai's compat entry point plus TypeBox's `Type`, `Static` and
* `TSchema`, which pi-ai's main entry no longer re-exports, and a `StringEnum` built with TypeBox (`TUnsafe<T>`), so
* extensions that use it inside `Type.Object` type-check and infer as before. The TypeBox is the coding agent's own,
* the same one extensions get for `typebox`.
*/
import { type TUnsafe, Type } from "typebox";
export * from "@earendil-works/pi-ai/compat";
export type { Static, TSchema } from "typebox";
export { Type };
/**
* Creates a string enum schema compatible with Google's API and other providers that don't support anyOf/const
* patterns: `{ type: "string", enum: [...] }`, typed for TypeBox's `Static`.
*/
export function StringEnum<T extends readonly string[]>(
values: T,
options?: { description?: string; default?: T[number] },
): TUnsafe<T[number]> {
return Type.Unsafe<T[number]>({
type: "string",
enum: values as unknown as T[number][],
...(options?.description && { description: options.description }),
...(options?.default && { default: options.default }),
});
}

View file

@ -1,5 +1,4 @@
import * as bundledPiAgentCore from "@earendil-works/pi-agent-core";
import * as bundledPiAiCompat from "@earendil-works/pi-ai/compat";
import * as bundledPiAiModels from "@earendil-works/pi-ai/models";
import * as bundledPiAiOauth from "@earendil-works/pi-ai/oauth";
import * as bundledPiAiProviders from "@earendil-works/pi-ai/providers/all";
@ -10,6 +9,7 @@ import * as bundledTypeboxValue from "typebox/value";
// This import is safe because loader.ts exports are not re-exported from index.ts.
// Extensions can therefore import from @earendil-works/pi-coding-agent.
import * as bundledPiCodingAgent from "../../index.ts";
import * as bundledPiAiExtensionView from "./pi-ai-extension-view.ts";
/** Modules available to extensions in source and compiled binary runtimes. */
export const VIRTUAL_MODULES: Record<string, unknown> = {
@ -22,18 +22,19 @@ export const VIRTUAL_MODULES: Record<string, unknown> = {
"@earendil-works/pi-agent-core": bundledPiAgentCore,
"@earendil-works/pi-tui": bundledPiTui,
// Extensions resolve the pi-ai root to the compat entrypoint (a strict
// superset of the core entrypoint): existing extensions using the old
// global API keep working at runtime until compat is removed.
"@earendil-works/pi-ai": bundledPiAiCompat,
"@earendil-works/pi-ai/compat": bundledPiAiCompat,
// superset of the core entrypoint) plus TypeBox's Type, Static and TSchema:
// existing extensions using the old global API and pi-ai's former TypeBox
// re-exports keep working.
"@earendil-works/pi-ai": bundledPiAiExtensionView,
"@earendil-works/pi-ai/compat": bundledPiAiExtensionView,
"@earendil-works/pi-ai/models": bundledPiAiModels,
"@earendil-works/pi-ai/oauth": bundledPiAiOauth,
"@earendil-works/pi-ai/providers/all": bundledPiAiProviders,
"@earendil-works/pi-coding-agent": bundledPiCodingAgent,
"@mariozechner/pi-agent-core": bundledPiAgentCore,
"@mariozechner/pi-tui": bundledPiTui,
"@mariozechner/pi-ai": bundledPiAiCompat,
"@mariozechner/pi-ai/compat": bundledPiAiCompat,
"@mariozechner/pi-ai": bundledPiAiExtensionView,
"@mariozechner/pi-ai/compat": bundledPiAiExtensionView,
"@mariozechner/pi-ai/models": bundledPiAiModels,
"@mariozechner/pi-ai/oauth": bundledPiAiOauth,
"@mariozechner/pi-ai/providers/all": bundledPiAiProviders,

View file

@ -1,20 +1,21 @@
/** Immutable, credential-blind models.json snapshot. */
import { readFile } from "node:fs/promises";
import { ProviderCompatSchema } from "@earendil-works/pi-ai/providers/compat-schema";
import {
ModelCostSchema,
ModelInputLimitsSchema,
ModelInputModalitySchema,
ModelPromptCacheSchema,
ThinkingLevelMapSchema,
} from "@earendil-works/pi-ai/providers/model-schema";
import { type Static, Type } from "typebox";
import { Compile } from "typebox/compile";
import type { TLocalizedValidationError } from "typebox/error";
import { stripJsonComments } from "../utils/json.ts";
import { normalizePath } from "../utils/paths.ts";
import { stripBom } from "../utils/text.ts";
import {
ModelCostSchema,
ModelInputLimitsSchema,
ModelInputModalitySchema,
ModelPromptCacheSchema,
PartialModelCostSchema,
ProviderCompatSchema,
ThinkingLevelMapSchema,
} from "./pi-ai-schemas.ts";
const SamplingParamsSchema = Type.Record(Type.String(), Type.Unknown());
const SamplingParamsByThinkingLevelSchema = Type.Object(
@ -74,7 +75,7 @@ const ModelOverrideSchema = Type.Object({
thinkingLevelMap: Type.Optional(ThinkingLevelMapSchema),
input: Type.Optional(Type.Array(ModelInputModalitySchema)),
inputLimits: Type.Optional(ModelInputLimitsSchema),
cost: Type.Optional(Type.Partial(ModelCostSchema)),
cost: Type.Optional(PartialModelCostSchema),
promptCache: Type.Optional(ModelPromptCacheSchema),
contextWindow: Type.Optional(
Type.Number({ exclusiveMinimum: 0, description: ModelFieldDescriptions.contextWindow }),

View file

@ -0,0 +1,40 @@
/**
* pi-ai's configuration schemas (built with pi-schema) as TypeBox schemas, for the coding agent's own TypeBox-built
* configuration schemas (models.json, settings.json): the same JSON, typed for TypeBox's `Static`.
*/
import {
type ProviderCompat,
ProviderCompatSchema as ProviderCompatJsonSchema,
} from "@earendil-works/pi-ai/providers/compat-schema";
import {
type ModelCost,
ModelCostSchema as ModelCostJsonSchema,
type ModelInputLimits,
ModelInputLimitsSchema as ModelInputLimitsJsonSchema,
type ModelInputModality,
ModelInputModalitySchema as ModelInputModalityJsonSchema,
type ModelPromptCache,
ModelPromptCacheSchema as ModelPromptCacheJsonSchema,
type ModelThinkingLevel,
ModelThinkingLevelSchema as ModelThinkingLevelJsonSchema,
type ThinkingLevelMap,
ThinkingLevelMapSchema as ThinkingLevelMapJsonSchema,
} from "@earendil-works/pi-ai/providers/model-schema";
import { Type } from "typebox";
export const ProviderCompatSchema = Type.Unsafe<ProviderCompat>(ProviderCompatJsonSchema);
export const ModelCostSchema = Type.Unsafe<ModelCost>(ModelCostJsonSchema);
export const ModelInputLimitsSchema = Type.Unsafe<ModelInputLimits>(ModelInputLimitsJsonSchema);
export const ModelInputModalitySchema = Type.Unsafe<ModelInputModality>(ModelInputModalityJsonSchema);
export const ModelPromptCacheSchema = Type.Unsafe<ModelPromptCache>(ModelPromptCacheJsonSchema);
export const ModelThinkingLevelSchema = Type.Unsafe<ModelThinkingLevel>(ModelThinkingLevelJsonSchema);
export const ThinkingLevelMapSchema = Type.Unsafe<ThinkingLevelMap>(ThinkingLevelMapJsonSchema);
/** `ModelCostSchema` with every field optional, for model overrides that change some of the rates. */
export const PartialModelCostSchema = Type.Unsafe<Partial<ModelCost>>(withoutRequired(ModelCostJsonSchema));
function withoutRequired(schema: object): object {
const { required: _required, ...rest } = schema as { required?: unknown };
return rest;
}

View file

@ -1,5 +1,5 @@
import { ModelThinkingLevelSchema } from "@earendil-works/pi-ai/providers/model-schema";
import { type Static, type TSchemaOptions, Type } from "typebox";
import { ModelThinkingLevelSchema } from "./pi-ai-schemas.ts";
import { SETTINGS_DEFAULTS } from "./settings-defaults.ts";
function nonNegativeSafeInteger(options: { default?: number } = {}) {

View file

@ -1,7 +1,6 @@
import { constants } from "node:fs";
import { access as fsAccess } from "node:fs/promises";
import { constants as osConstants } from "node:os";
import type { AgentTool } from "@earendil-works/pi-agent-core";
import { spawn } from "child_process";
import { type Static, Type } from "typebox";
import { waitForChildProcess } from "../../utils/child-process.ts";
@ -16,7 +15,7 @@ import {
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
import { OutputAccumulator } from "./output-accumulator.ts";
import { BASH_UPDATE_THROTTLE_MS, createShellRenderers } from "./renderers/bash.ts";
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
import { type TypeBoxAgentTool, wrapToolDefinition } from "./tool-definition-wrapper.ts";
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from "./truncate.ts";
const MAX_TIMEOUT_MS = 2_147_483_647;
@ -431,7 +430,7 @@ export function createBashToolDefinition(
return createShellToolDefinition(cwd, bashToolConfig, options);
}
export function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {
export function createBashTool(cwd: string, options?: BashToolOptions): TypeBoxAgentTool<typeof bashSchema> {
const definition = createBashToolDefinition(cwd, options);
const tool = wrapToolDefinition(definition);
Object.assign(tool, {

View file

@ -1,4 +1,3 @@
import type { AgentTool } from "@earendil-works/pi-agent-core";
import { constants } from "fs";
import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
import { type Static, Type } from "typebox";
@ -16,7 +15,7 @@ import {
import { withFileMutationQueue } from "./file-mutation-queue.ts";
import { resolveToCwd } from "./path-utils.ts";
import { type EditRenderState, editRenderers } from "./renderers/edit.ts";
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
import { type TypeBoxAgentTool, wrapToolDefinition } from "./tool-definition-wrapper.ts";
const replaceEditSchema = Type.Object(
{
@ -213,6 +212,6 @@ export function createEditToolDefinition(
};
}
export function createEditTool(cwd: string, options?: EditToolOptions): AgentTool<typeof editSchema> {
export function createEditTool(cwd: string, options?: EditToolOptions): TypeBoxAgentTool<typeof editSchema> {
return wrapToolDefinition(createEditToolDefinition(cwd, options));
}

View file

@ -1,5 +1,4 @@
import { createInterface } from "node:readline";
import type { AgentTool } from "@earendil-works/pi-agent-core";
import { spawn } from "child_process";
import path from "path";
import { type Static, Type } from "typebox";
@ -7,7 +6,7 @@ import { ensureTool } from "../../utils/tools-manager.ts";
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
import { pathExists, resolveToCwd } from "./path-utils.ts";
import { findRenderers } from "./renderers/find.ts";
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
import { type TypeBoxAgentTool, wrapToolDefinition } from "./tool-definition-wrapper.ts";
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
/** Relativize a find result against the search root and normalize it to posix separators. */
@ -313,6 +312,6 @@ export function createFindToolDefinition(
};
}
export function createFindTool(cwd: string, options?: FindToolOptions): AgentTool<typeof findSchema> {
export function createFindTool(cwd: string, options?: FindToolOptions): TypeBoxAgentTool<typeof findSchema> {
return wrapToolDefinition(createFindToolDefinition(cwd, options));
}

View file

@ -1,6 +1,5 @@
import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
import { createInterface } from "node:readline";
import type { AgentTool } from "@earendil-works/pi-agent-core";
import { spawn } from "child_process";
import path from "path";
import { type Static, Type } from "typebox";
@ -8,7 +7,7 @@ import { ensureTool } from "../../utils/tools-manager.ts";
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
import { resolveToCwd } from "./path-utils.ts";
import { grepRenderers } from "./renderers/grep.ts";
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
import { type TypeBoxAgentTool, wrapToolDefinition } from "./tool-definition-wrapper.ts";
import {
DEFAULT_MAX_BYTES,
formatSize,
@ -320,6 +319,6 @@ export function createGrepToolDefinition(
};
}
export function createGrepTool(cwd: string, options?: GrepToolOptions): AgentTool<typeof grepSchema> {
export function createGrepTool(cwd: string, options?: GrepToolOptions): TypeBoxAgentTool<typeof grepSchema> {
return wrapToolDefinition(createGrepToolDefinition(cwd, options));
}

View file

@ -1,11 +1,10 @@
import { readdir as fsReaddir, stat as fsStat } from "node:fs/promises";
import type { AgentTool } from "@earendil-works/pi-agent-core";
import nodePath from "path";
import { type Static, Type } from "typebox";
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
import { pathExists, resolveToCwd } from "./path-utils.ts";
import { lsRenderers } from "./renderers/ls.ts";
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
import { type TypeBoxAgentTool, wrapToolDefinition } from "./tool-definition-wrapper.ts";
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
const lsSchema = Type.Object({
@ -170,6 +169,6 @@ export function createLsToolDefinition(
};
}
export function createLsTool(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema> {
export function createLsTool(cwd: string, options?: LsToolOptions): TypeBoxAgentTool<typeof lsSchema> {
return wrapToolDefinition(createLsToolDefinition(cwd, options));
}

View file

@ -1,4 +1,3 @@
import type { AgentTool } from "@earendil-works/pi-agent-core";
import type { Api, ImageContent, Model, ModelImageResizeOptions, TextContent } from "@earendil-works/pi-ai";
import { constants } from "fs";
import { access as fsAccess, readFile as fsReadFile } from "fs/promises";
@ -8,7 +7,7 @@ import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.ts";
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
import { resolveReadPathAsync } from "./path-utils.ts";
import { readRenderers } from "./renderers/read.ts";
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
import { type TypeBoxAgentTool, wrapToolDefinition } from "./tool-definition-wrapper.ts";
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
const readSchema = Type.Object({
@ -219,6 +218,6 @@ export function createReadToolDefinition(
};
}
export function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {
export function createReadTool(cwd: string, options?: ReadToolOptions): TypeBoxAgentTool<typeof readSchema> {
return wrapToolDefinition(createReadToolDefinition(cwd, options));
}

View file

@ -1,6 +1,25 @@
import type { AgentTool } from "@earendil-works/pi-agent-core";
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
import type { Static, TSchema } from "typebox";
import type { ExtensionToolContext, ToolDefinition } from "../extensions/types.ts";
/**
* An `AgentTool` with a TypeBox parameter schema whose `execute` and `prepareArguments` are typed by TypeBox's
* `Static`, as the built-in tools are (pi-agent-core types `params` through pi-schema, which gives `unknown` for
* TypeBox schemas). Spreads into a `ToolDefinition` of the same schema.
*/
export type TypeBoxAgentTool<TParams extends TSchema = TSchema, TDetails = any> = Omit<
AgentTool<TParams, TDetails>,
"prepareArguments" | "execute"
> & {
prepareArguments?: (args: unknown) => Static<TParams>;
execute: (
toolCallId: string,
params: Static<TParams>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>,
) => Promise<AgentToolResult<TDetails>>;
};
/** Creates the context for one tool call. */
export type ToolContextFactory = (toolCallId: string, signal: AbortSignal | undefined) => ExtensionToolContext;

View file

@ -1,4 +1,3 @@
import type { AgentTool } from "@earendil-works/pi-agent-core";
import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises";
import { dirname } from "path";
import { type Static, Type } from "typebox";
@ -6,7 +5,7 @@ import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
import { withFileMutationQueue } from "./file-mutation-queue.ts";
import { resolveToCwd } from "./path-utils.ts";
import { writeRenderers } from "./renderers/write.ts";
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
import { type TypeBoxAgentTool, wrapToolDefinition } from "./tool-definition-wrapper.ts";
const writeSchema = Type.Object({
path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
@ -92,6 +91,6 @@ export function createWriteToolDefinition(
};
}
export function createWriteTool(cwd: string, options?: WriteToolOptions): AgentTool<typeof writeSchema> {
export function createWriteTool(cwd: string, options?: WriteToolOptions): TypeBoxAgentTool<typeof writeSchema> {
return wrapToolDefinition(createWriteToolDefinition(cwd, options));
}

View file

@ -32,7 +32,7 @@ import type { ToolDefinition, ToolLoadout, ToolLoadoutChanges, ToolNamespace } f
import type { ModelRegistry } from "../../core/model-registry.ts";
import { SETTINGS_DEFAULTS } from "../../core/settings-defaults.ts";
import type { CodemodeMode } from "../../core/settings-manager.ts";
import { wrapToolDefinition } from "../../core/tools/tool-definition-wrapper.ts";
import { type TypeBoxAgentTool, wrapToolDefinition } from "../../core/tools/tool-definition-wrapper.ts";
import { loadCodemodeExecutor } from "./execute.lazy.ts";
import { codemodeRenderers } from "./renderer.ts";
@ -422,7 +422,7 @@ export function createCodemodeToolDefinition(
export function createCodemodeTool(
tools: readonly AgentTool<any>[] = [],
options: CodemodeToolOptions = {},
): AgentTool<typeof codemodeSchema> {
): TypeBoxAgentTool<typeof codemodeSchema> {
const definition = createCodemodeToolDefinition(options);
const tool = wrapToolDefinition(definition);
Object.assign(tool, {

View file

@ -21,8 +21,8 @@ import {
getModel,
type Model,
type SimpleStreamOptions,
Type,
} from "@earendil-works/pi-ai/compat";
import { Type } from "typebox";
import {
getOpenAICodexWebSocketDebugStats,
streamSimple as streamSimpleOpenAICodexResponses,

View file

@ -18,7 +18,10 @@ import { Type } from "typebox";
import { afterEach, describe, expect, it } from "vitest";
import type { ExtensionAPI } from "../../src/core/extensions/types.ts";
import type { CustomEntry } from "../../src/core/session-manager.ts";
import { createToolDefinitionFromAgentTool } from "../../src/core/tools/tool-definition-wrapper.ts";
import {
createToolDefinitionFromAgentTool,
type TypeBoxAgentTool,
} from "../../src/core/tools/tool-definition-wrapper.ts";
import { readCodemodeStore } from "../../src/extensions/codemode/execute.ts";
import { createCodemodeExtension } from "../../src/extensions/codemode/index.ts";
import {
@ -33,7 +36,7 @@ const TINY_PNG_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
const echoSchema = Type.Object({ text: Type.String({ description: "Text to echo" }) });
const echoTool: AgentTool<typeof echoSchema> = {
const echoTool: TypeBoxAgentTool<typeof echoSchema> = {
name: "echo",
label: "Echo",
description: "Echo text back.\n\nSecond paragraph.",