From 35ff2689ee0a74009db73c697025de88ad86f6fb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 22 Apr 2026 19:59:33 +0200 Subject: [PATCH] fix(typebox): migrate to v1 with extension compat (#3474) * fix(typebox): migrate to v1 with extension compat Replace AJV-based validation with TypeBox-native validation, keep legacy extension imports working (including @sinclair/typebox/compiler), and restore coercion for serialized/plain JSON schemas. This change closes #3112. * fix(typebox): use canonical imports and harden coercion Switch first-party code to canonical typebox imports while retaining legacy extension aliases in the loader. Remove obsolete runtime codegen guards, expand serialized JSON-schema coercion coverage, and update related tests and fixtures. Fixes #3112. --------- Co-authored-by: Mario Zechner --- package-lock.json | 97 +----- packages/agent/package.json | 3 +- packages/agent/src/types.ts | 2 +- packages/agent/test/agent-loop.test.ts | 2 +- packages/agent/test/utils/calculate.ts | 2 +- packages/agent/test/utils/get-current-time.ts | 2 +- packages/ai/README.md | 2 +- packages/ai/package.json | 4 +- packages/ai/src/index.ts | 4 +- packages/ai/src/providers/amazon-bedrock.ts | 3 +- packages/ai/src/types.ts | 2 +- packages/ai/src/utils/typebox-helpers.ts | 2 +- packages/ai/src/utils/validation.ts | 325 +++++++++++++++--- .../anthropic-tool-name-normalization.test.ts | 2 +- .../ai/test/cross-provider-handoff.test.ts | 2 +- .../google-tool-call-missing-args.test.ts | 2 +- packages/ai/test/image-tool-result.test.ts | 2 +- packages/ai/test/interleaved-thinking.test.ts | 2 +- packages/ai/test/mistral-tool-schema.test.ts | 2 +- ...i-completions-cache-control-format.test.ts | 2 +- .../openai-completions-tool-choice.test.ts | 2 +- ...nai-responses-reasoning-replay-e2e.test.ts | 2 +- ...penai-responses-tool-result-images.test.ts | 2 +- packages/ai/test/stream.test.ts | 2 +- .../test/tool-call-id-normalization.test.ts | 2 +- .../ai/test/tool-call-without-result.test.ts | 2 +- packages/ai/test/unicode-surrogate.test.ts | 2 +- packages/ai/test/validation.test.ts | 99 +++++- .../extensions/antigravity-image-gen.ts | 2 +- .../examples/extensions/dynamic-tools.ts | 2 +- .../examples/extensions/question.ts | 2 +- .../examples/extensions/questionnaire.ts | 2 +- .../examples/extensions/reload-runtime.ts | 2 +- .../examples/extensions/shutdown-command.ts | 2 +- .../examples/extensions/subagent/index.ts | 2 +- .../examples/extensions/tic-tac-toe.ts | 2 +- .../coding-agent/examples/extensions/todo.ts | 2 +- .../examples/extensions/tool-override.ts | 2 +- .../examples/extensions/truncated-tool.ts | 2 +- .../examples/extensions/with-deps/index.ts | 2 +- packages/coding-agent/package.json | 3 +- .../src/core/extensions/loader.ts | 36 +- .../extensions/typebox-compiler-compat.ts | 19 + .../coding-agent/src/core/extensions/types.ts | 4 +- .../coding-agent/src/core/model-registry.ts | 37 +- packages/coding-agent/src/core/tools/bash.ts | 2 +- packages/coding-agent/src/core/tools/edit.ts | 2 +- packages/coding-agent/src/core/tools/find.ts | 2 +- packages/coding-agent/src/core/tools/grep.ts | 2 +- packages/coding-agent/src/core/tools/ls.ts | 2 +- packages/coding-agent/src/core/tools/read.ts | 2 +- packages/coding-agent/src/core/tools/write.ts | 2 +- .../src/modes/interactive/theme/theme.ts | 32 +- .../test/agent-session-concurrent.test.ts | 2 +- .../test/agent-session-dynamic-tools.test.ts | 2 +- .../test/agent-session-retry.test.ts | 2 +- .../test/extensions-discovery.test.ts | 2 +- .../test/extensions-runner.test.ts | 6 +- .../coding-agent/test/resource-loader.test.ts | 8 +- .../test/stdout-cleanliness.test.ts | 1 + .../agent-session-bash-persistence.test.ts | 2 +- .../agent-session-model-extension.test.ts | 2 +- .../test/suite/agent-session-prompt.test.ts | 2 +- .../test/suite/agent-session-queue.test.ts | 2 +- .../suite/agent-session-retry-events.test.ts | 2 +- ...2023-queued-slash-command-followup.test.ts | 2 +- ...-allowlist-filters-extension-tools.test.ts | 2 +- .../coding-agent/test/test-harness.test.ts | 2 +- .../test/tool-execution-component.test.ts | 2 +- packages/coding-agent/tsconfig.examples.json | 2 +- packages/coding-agent/vitest.config.ts | 34 +- packages/mom/package.json | 2 +- packages/mom/src/tools/attach.ts | 2 +- packages/mom/src/tools/bash.ts | 2 +- packages/mom/src/tools/edit.ts | 2 +- packages/mom/src/tools/read.ts | 2 +- packages/mom/src/tools/write.ts | 2 +- packages/web-ui/package.json | 1 + .../web-ui/src/tools/artifacts/artifacts.ts | 2 +- packages/web-ui/src/tools/extract-document.ts | 2 +- packages/web-ui/src/tools/javascript-repl.ts | 2 +- tsconfig.json | 2 +- 82 files changed, 580 insertions(+), 264 deletions(-) create mode 100644 packages/coding-agent/src/core/extensions/typebox-compiler-compat.ts diff --git a/package-lock.json b/package-lock.json index 95f702333..8ec61b3ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2868,12 +2868,6 @@ "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", "license": "Apache-2.0" }, - "node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", - "license": "MIT" - }, "node_modules/@slack/logger": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.1.tgz", @@ -4262,39 +4256,6 @@ "node": ">= 14" } }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/alien-signals": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-2.0.8.tgz", @@ -5313,12 +5274,6 @@ "@types/yauzl": "^2.9.1" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -5336,22 +5291,6 @@ "node": ">=8.6.0" } }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fast-xml-builder": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", @@ -6067,12 +6006,6 @@ "node": ">=16" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, "node_modules/jsonschema": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", @@ -7295,15 +7228,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -8098,6 +8022,12 @@ "node": "*" } }, + "node_modules/typebox": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.29.tgz", + "integrity": "sha512-zPwgxCka1v976Zn2Hg0THUHY6JFPzbBkz12ZOZeyc/vcoeqLyeFEsXw5zGg/nMhbNE/rOEALVKRTlHIEDR3Jhw==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -8597,7 +8527,8 @@ "version": "0.68.1", "license": "MIT", "dependencies": { - "@mariozechner/pi-ai": "^0.68.1" + "@mariozechner/pi-ai": "^0.68.1", + "typebox": "^1.1.24" }, "devDependencies": { "@types/node": "^24.3.0", @@ -8634,13 +8565,11 @@ "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "^2.2.0", - "@sinclair/typebox": "^0.34.41", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", "chalk": "^5.6.2", "openai": "6.26.0", "partial-json": "^0.1.7", "proxy-agent": "^6.5.0", + "typebox": "^1.1.24", "undici": "^7.19.1", "zod-to-json-schema": "^3.24.6" }, @@ -8683,8 +8612,6 @@ "@mariozechner/pi-ai": "^0.68.1", "@mariozechner/pi-tui": "^0.68.1", "@silvia-odwyer/photon-node": "^0.3.4", - "@sinclair/typebox": "^0.34.41", - "ajv": "^8.17.1", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", "diff": "^8.0.2", @@ -8697,6 +8624,7 @@ "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", "strip-ansi": "^7.1.0", + "typebox": "^1.1.24", "undici": "^7.19.1", "uuid": "^11.1.0", "yaml": "^2.8.2" @@ -8781,12 +8709,12 @@ "@mariozechner/pi-agent-core": "^0.68.1", "@mariozechner/pi-ai": "^0.68.1", "@mariozechner/pi-coding-agent": "^0.68.1", - "@sinclair/typebox": "^0.34.0", "@slack/socket-mode": "^2.0.0", "@slack/web-api": "^7.0.0", "chalk": "^5.6.2", "croner": "^9.1.0", - "diff": "^8.0.2" + "diff": "^8.0.2", + "typebox": "^1.1.24" }, "bin": { "mom": "dist/main.js" @@ -8893,6 +8821,7 @@ "lucide": "^0.544.0", "ollama": "^0.6.0", "pdfjs-dist": "5.4.394", + "typebox": "^1.1.24", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" }, "devDependencies": { diff --git a/packages/agent/package.json b/packages/agent/package.json index e46692dab..36f57766c 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -17,7 +17,8 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@mariozechner/pi-ai": "^0.68.1" + "@mariozechner/pi-ai": "^0.68.1", + "typebox": "^1.1.24" }, "keywords": [ "ai", diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index 193fc1f8e..fc7882923 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -10,7 +10,7 @@ import type { Tool, ToolResultMessage, } from "@mariozechner/pi-ai"; -import type { Static, TSchema } from "@sinclair/typebox"; +import type { Static, TSchema } from "typebox"; /** * Stream function used by the agent loop. diff --git a/packages/agent/test/agent-loop.test.ts b/packages/agent/test/agent-loop.test.ts index 33c1fb286..56291787e 100644 --- a/packages/agent/test/agent-loop.test.ts +++ b/packages/agent/test/agent-loop.test.ts @@ -6,7 +6,7 @@ import { type Model, type UserMessage, } from "@mariozechner/pi-ai"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { agentLoop, agentLoopContinue } from "../src/agent-loop.js"; import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, AgentTool } from "../src/types.js"; diff --git a/packages/agent/test/utils/calculate.ts b/packages/agent/test/utils/calculate.ts index af5b17c4b..cd2a3777b 100644 --- a/packages/agent/test/utils/calculate.ts +++ b/packages/agent/test/utils/calculate.ts @@ -1,4 +1,4 @@ -import { type Static, Type } from "@sinclair/typebox"; +import { type Static, Type } from "typebox"; import type { AgentTool, AgentToolResult } from "../../src/types.js"; export interface CalculateResult extends AgentToolResult { diff --git a/packages/agent/test/utils/get-current-time.ts b/packages/agent/test/utils/get-current-time.ts index 814233b14..b7075d9bc 100644 --- a/packages/agent/test/utils/get-current-time.ts +++ b/packages/agent/test/utils/get-current-time.ts @@ -1,4 +1,4 @@ -import { type Static, Type } from "@sinclair/typebox"; +import { type Static, Type } from "typebox"; import type { AgentTool, AgentToolResult } from "../../src/types.js"; export interface GetCurrentTimeResult extends AgentToolResult<{ utcTimestamp: number }> {} diff --git a/packages/ai/README.md b/packages/ai/README.md index b2d6d594f..8654ad987 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -202,7 +202,7 @@ for (const block of response.content) { ## Tools -Tools enable LLMs to interact with external systems. This library uses TypeBox schemas for type-safe tool definitions with automatic validation using AJV. TypeBox schemas can be serialized and deserialized as plain JSON, making them ideal for distributed systems. +Tools enable LLMs to interact with external systems. This library uses TypeBox schemas for type-safe tool definitions with automatic validation using TypeBox's built-in validator and value conversion utilities. TypeBox schemas can be serialized and deserialized as plain JSON, making them ideal for distributed systems. ### Defining Tools diff --git a/packages/ai/package.json b/packages/ai/package.json index dccaeb0ba..0b22daca6 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -76,9 +76,7 @@ "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "^2.2.0", - "@sinclair/typebox": "^0.34.41", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", + "typebox": "^1.1.24", "chalk": "^5.6.2", "openai": "6.26.0", "partial-json": "^0.1.7", diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 6a1ee2c01..7235ecc7a 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,5 +1,5 @@ -export type { Static, TSchema } from "@sinclair/typebox"; -export { Type } from "@sinclair/typebox"; +export type { Static, TSchema } from "typebox"; +export { Type } from "typebox"; export * from "./api-registry.js"; export * from "./env-api-keys.js"; diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index c76dfbf68..27fd7ccd3 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -20,6 +20,7 @@ import { type ToolConfiguration, ToolResultStatus, } from "@aws-sdk/client-bedrock-runtime"; +import type { DocumentType } from "@smithy/types"; import { calculateCost } from "../models.js"; import type { Api, @@ -760,7 +761,7 @@ function convertToolConfig( toolSpec: { name: tool.name, description: tool.description, - inputSchema: { json: tool.parameters }, + inputSchema: { json: tool.parameters as unknown as DocumentType }, }, })); diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 2ba2e71c1..aa45ba96c 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -223,7 +223,7 @@ export interface ToolResultMessage { export type Message = UserMessage | AssistantMessage | ToolResultMessage; -import type { TSchema } from "@sinclair/typebox"; +import type { TSchema } from "typebox"; export interface Tool { name: string; diff --git a/packages/ai/src/utils/typebox-helpers.ts b/packages/ai/src/utils/typebox-helpers.ts index 60e8aa69e..85a53555c 100644 --- a/packages/ai/src/utils/typebox-helpers.ts +++ b/packages/ai/src/utils/typebox-helpers.ts @@ -1,4 +1,4 @@ -import { type TUnsafe, Type } from "@sinclair/typebox"; +import { type TUnsafe, Type } from "typebox"; /** * Creates a string enum schema compatible with Google's API and other providers diff --git a/packages/ai/src/utils/validation.ts b/packages/ai/src/utils/validation.ts index 3a54fa13c..6fee28686 100644 --- a/packages/ai/src/utils/validation.ts +++ b/packages/ai/src/utils/validation.ts @@ -1,42 +1,270 @@ -import AjvModule from "ajv"; -import addFormatsModule from "ajv-formats"; - -// Handle both default and named exports -const Ajv = (AjvModule as any).default || AjvModule; -const addFormats = (addFormatsModule as any).default || addFormatsModule; - +import { Compile } from "typebox/compile"; +import type { TLocalizedValidationError } from "typebox/error"; +import { Value } from "typebox/value"; import type { Tool, ToolCall } from "../types.js"; -// Detect if we're in a browser extension environment with strict CSP -// Chrome extensions with Manifest V3 don't allow eval/Function constructor -const isBrowserExtension = typeof globalThis !== "undefined" && (globalThis as any).chrome?.runtime?.id !== undefined; +const validatorCache = new WeakMap>(); +const TYPEBOX_KIND = Symbol.for("TypeBox.Kind"); -function canUseRuntimeCodegen(): boolean { - if (isBrowserExtension) { - return false; +interface JsonSchemaObject { + type?: string | string[]; + properties?: Record; + items?: JsonSchemaObject | JsonSchemaObject[]; + additionalProperties?: boolean | JsonSchemaObject; + allOf?: JsonSchemaObject[]; + anyOf?: JsonSchemaObject[]; + oneOf?: JsonSchemaObject[]; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function isJsonSchemaObject(value: unknown): value is JsonSchemaObject { + return isRecord(value); +} + +function hasTypeBoxMetadata(schema: unknown): boolean { + return isRecord(schema) && Object.getOwnPropertySymbols(schema).includes(TYPEBOX_KIND); +} + +function getSchemaTypes(schema: JsonSchemaObject): string[] { + if (typeof schema.type === "string") { + return [schema.type]; } + if (Array.isArray(schema.type)) { + return schema.type.filter((type): type is string => typeof type === "string"); + } + return []; +} - try { - new Function("return true;"); - return true; - } catch { - return false; +function matchesJsonType(value: unknown, type: string): boolean { + switch (type) { + case "number": + return typeof value === "number"; + case "integer": + return typeof value === "number" && Number.isInteger(value); + case "boolean": + return typeof value === "boolean"; + case "string": + return typeof value === "string"; + case "null": + return value === null; + case "array": + return Array.isArray(value); + case "object": + return isRecord(value) && !Array.isArray(value); + default: + return false; } } -// Create a singleton AJV instance with formats only when runtime code generation is available. -let ajv: any = null; -if (canUseRuntimeCodegen()) { - try { - ajv = new Ajv({ - allErrors: true, - strict: false, - coerceTypes: true, - }); - addFormats(ajv); - } catch (_e) { - console.warn("AJV validation disabled due to CSP restrictions"); +function isValidatorSchema(value: unknown): value is Tool["parameters"] { + return isRecord(value); +} + +function getSubSchemaValidator(schema: JsonSchemaObject): ReturnType | undefined { + if (!isValidatorSchema(schema)) { + return undefined; } + try { + return getValidator(schema); + } catch { + return undefined; + } +} + +function coercePrimitiveByType(value: unknown, type: string): unknown { + switch (type) { + case "number": { + if (value === null) { + return 0; + } + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value); + if (Number.isFinite(parsed)) { + return parsed; + } + } + if (typeof value === "boolean") { + return value ? 1 : 0; + } + return value; + } + case "integer": { + if (value === null) { + return 0; + } + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value); + if (Number.isInteger(parsed)) { + return parsed; + } + } + if (typeof value === "boolean") { + return value ? 1 : 0; + } + return value; + } + case "boolean": { + if (value === null) { + return false; + } + if (typeof value === "string") { + if (value === "true") { + return true; + } + if (value === "false") { + return false; + } + } + if (typeof value === "number") { + if (value === 1) { + return true; + } + if (value === 0) { + return false; + } + } + return value; + } + case "string": { + if (value === null) { + return ""; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + return value; + } + case "null": { + if (value === "" || value === 0 || value === false) { + return null; + } + return value; + } + default: + return value; + } +} + +function applySchemaObjectCoercion(value: Record, schema: JsonSchemaObject): void { + const properties = schema.properties; + const definedKeys = new Set(properties ? Object.keys(properties) : []); + + if (properties) { + for (const [key, propertySchema] of Object.entries(properties)) { + if (!(key in value)) { + continue; + } + value[key] = coerceWithJsonSchema(value[key], propertySchema); + } + } + + if (schema.additionalProperties && isJsonSchemaObject(schema.additionalProperties)) { + for (const [key, propertyValue] of Object.entries(value)) { + if (definedKeys.has(key)) { + continue; + } + value[key] = coerceWithJsonSchema(propertyValue, schema.additionalProperties); + } + } +} + +function applySchemaArrayCoercion(value: unknown[], schema: JsonSchemaObject): void { + if (Array.isArray(schema.items)) { + for (let index = 0; index < value.length; index++) { + const itemSchema = schema.items[index]; + if (!itemSchema) { + continue; + } + value[index] = coerceWithJsonSchema(value[index], itemSchema); + } + return; + } + + if (isJsonSchemaObject(schema.items)) { + for (let index = 0; index < value.length; index++) { + value[index] = coerceWithJsonSchema(value[index], schema.items); + } + } +} + +function coerceWithUnionSchema(value: unknown, schemas: JsonSchemaObject[]): unknown { + for (const schema of schemas) { + const candidate = structuredClone(value); + const coerced = coerceWithJsonSchema(candidate, schema); + const validator = getSubSchemaValidator(schema); + if (validator?.Check(coerced)) { + return coerced; + } + } + return value; +} + +function coerceWithJsonSchema(value: unknown, schema: JsonSchemaObject): unknown { + let nextValue = value; + + if (Array.isArray(schema.allOf)) { + for (const nested of schema.allOf) { + nextValue = coerceWithJsonSchema(nextValue, nested); + } + } + + if (Array.isArray(schema.anyOf)) { + nextValue = coerceWithUnionSchema(nextValue, schema.anyOf); + } + + if (Array.isArray(schema.oneOf)) { + nextValue = coerceWithUnionSchema(nextValue, schema.oneOf); + } + + const schemaTypes = getSchemaTypes(schema); + const matchesUnionMember = + schemaTypes.length > 1 && schemaTypes.some((schemaType) => matchesJsonType(nextValue, schemaType)); + if (schemaTypes.length > 0 && !matchesUnionMember) { + for (const schemaType of schemaTypes) { + const candidate = coercePrimitiveByType(nextValue, schemaType); + if (candidate !== nextValue) { + nextValue = candidate; + break; + } + } + } + + if (schemaTypes.includes("object") && isRecord(nextValue) && !Array.isArray(nextValue)) { + applySchemaObjectCoercion(nextValue, schema); + } + + if (schemaTypes.includes("array") && Array.isArray(nextValue)) { + applySchemaArrayCoercion(nextValue, schema); + } + + return nextValue; +} + +function getValidator(schema: Tool["parameters"]): ReturnType { + const key = schema as object; + const cached = validatorCache.get(key); + if (cached) { + return cached; + } + const validator = Compile(schema); + validatorCache.set(key, validator); + return validator; +} + +function formatValidationPath(error: TLocalizedValidationError): string { + if (error.keyword === "required") { + const requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties; + const requiredProperty = requiredProperties?.[0]; + if (requiredProperty) { + const basePath = error.instancePath.replace(/^\//, "").replace(/\//g, "."); + return basePath ? `${basePath}.${requiredProperty}` : requiredProperty; + } + } + const path = error.instancePath.replace(/^\//, "").replace(/\//g, "."); + return path || "root"; } /** @@ -62,29 +290,32 @@ export function validateToolCall(tools: Tool[], toolCall: ToolCall): any { * @throws Error with formatted message if validation fails */ export function validateToolArguments(tool: Tool, toolCall: ToolCall): any { - // Skip validation in environments where runtime code generation is unavailable. - if (!ajv || !canUseRuntimeCodegen()) { - return toolCall.arguments; + const args = structuredClone(toolCall.arguments); + Value.Convert(tool.parameters, args); + + const validator = getValidator(tool.parameters); + if (!hasTypeBoxMetadata(tool.parameters) && isJsonSchemaObject(tool.parameters)) { + const coerced = coerceWithJsonSchema(args, tool.parameters); + if (coerced !== args) { + if (isRecord(args) && isRecord(coerced)) { + for (const key of Object.keys(args)) { + delete args[key]; + } + Object.assign(args, coerced); + } else { + return validator.Check(coerced) ? coerced : args; + } + } } - // Compile the schema. - const validate = ajv.compile(tool.parameters); - - // Clone arguments so AJV can safely mutate for type coercion - const args = structuredClone(toolCall.arguments); - - // Validate the arguments (AJV mutates args in-place for type coercion) - if (validate(args)) { + if (validator.Check(args)) { return args; } - // Format validation errors nicely const errors = - validate.errors - ?.map((err: any) => { - const path = err.instancePath ? err.instancePath.substring(1) : err.params.missingProperty || "root"; - return ` - ${path}: ${err.message}`; - }) + validator + .Errors(args) + .map((error) => ` - ${formatValidationPath(error)}: ${error.message}`) .join("\n") || "Unknown validation error"; const errorMessage = `Validation failed for tool "${toolCall.name}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(toolCall.arguments, null, 2)}`; diff --git a/packages/ai/test/anthropic-tool-name-normalization.test.ts b/packages/ai/test/anthropic-tool-name-normalization.test.ts index 93241b5f3..0a6c6bd38 100644 --- a/packages/ai/test/anthropic-tool-name-normalization.test.ts +++ b/packages/ai/test/anthropic-tool-name-normalization.test.ts @@ -1,4 +1,4 @@ -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; import { stream } from "../src/stream.js"; diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index f4e4cc263..d9e1b9886 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -22,8 +22,8 @@ * Fixtures are generated fresh on each run. */ -import { Type } from "@sinclair/typebox"; import { writeFileSync } from "fs"; +import { Type } from "typebox"; import { beforeAll, describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; import { completeSimple, getEnvApiKey } from "../src/stream.js"; diff --git a/packages/ai/test/google-tool-call-missing-args.test.ts b/packages/ai/test/google-tool-call-missing-args.test.ts index e8a2296a9..2082502f4 100644 --- a/packages/ai/test/google-tool-call-missing-args.test.ts +++ b/packages/ai/test/google-tool-call-missing-args.test.ts @@ -1,4 +1,4 @@ -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { afterEach, describe, expect, it, vi } from "vitest"; import { streamGoogleGeminiCli } from "../src/providers/google-gemini-cli.js"; import type { Context, Model, ToolCall } from "../src/types.js"; diff --git a/packages/ai/test/image-tool-result.test.ts b/packages/ai/test/image-tool-result.test.ts index da5f12e3d..3b02dd55e 100644 --- a/packages/ai/test/image-tool-result.test.ts +++ b/packages/ai/test/image-tool-result.test.ts @@ -1,6 +1,6 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import type { Api, Context, Model, Tool, ToolResultMessage } from "../src/index.js"; import { complete, getModel } from "../src/index.js"; diff --git a/packages/ai/test/interleaved-thinking.test.ts b/packages/ai/test/interleaved-thinking.test.ts index e6c775402..fabc3c40b 100644 --- a/packages/ai/test/interleaved-thinking.test.ts +++ b/packages/ai/test/interleaved-thinking.test.ts @@ -1,4 +1,4 @@ -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getEnvApiKey } from "../src/env-api-keys.js"; import { getModel } from "../src/models.js"; diff --git a/packages/ai/test/mistral-tool-schema.test.ts b/packages/ai/test/mistral-tool-schema.test.ts index 726aca4c2..623e9a3a5 100644 --- a/packages/ai/test/mistral-tool-schema.test.ts +++ b/packages/ai/test/mistral-tool-schema.test.ts @@ -1,4 +1,4 @@ -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; import { complete } from "../src/stream.js"; diff --git a/packages/ai/test/openai-completions-cache-control-format.test.ts b/packages/ai/test/openai-completions-cache-control-format.test.ts index 92ae6c398..de69d02ac 100644 --- a/packages/ai/test/openai-completions-cache-control-format.test.ts +++ b/packages/ai/test/openai-completions-cache-control-format.test.ts @@ -1,4 +1,4 @@ -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { getModel } from "../src/models.js"; import { streamOpenAICompletions } from "../src/providers/openai-completions.js"; diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 485e973e8..fdd92d482 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -1,4 +1,4 @@ -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { getModel } from "../src/models.js"; import { streamSimple } from "../src/stream.js"; diff --git a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts index 4d6899d4f..2f78ddc78 100644 --- a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts +++ b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts @@ -1,4 +1,4 @@ -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; import { complete, getEnvApiKey } from "../src/stream.js"; diff --git a/packages/ai/test/openai-responses-tool-result-images.test.ts b/packages/ai/test/openai-responses-tool-result-images.test.ts index c5e85fe68..05d9c16ff 100644 --- a/packages/ai/test/openai-responses-tool-result-images.test.ts +++ b/packages/ai/test/openai-responses-tool-result-images.test.ts @@ -1,8 +1,8 @@ import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { Type } from "@sinclair/typebox"; import type { ResponseFunctionCallOutputItemList } from "openai/resources/responses/responses.js"; +import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import type { Api, Context, Model, StreamOptions, Tool, ToolResultMessage } from "../src/index.js"; import { complete, getModel } from "../src/index.js"; diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 35029c0a2..2066ea693 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -1,7 +1,7 @@ -import { Type } from "@sinclair/typebox"; import { type ChildProcess, execSync, spawn } from "child_process"; import { readFileSync } from "fs"; import { dirname, join } from "path"; +import { Type } from "typebox"; import { fileURLToPath } from "url"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; diff --git a/packages/ai/test/tool-call-id-normalization.test.ts b/packages/ai/test/tool-call-id-normalization.test.ts index 858a96c8e..b6e739e7c 100644 --- a/packages/ai/test/tool-call-id-normalization.test.ts +++ b/packages/ai/test/tool-call-id-normalization.test.ts @@ -10,7 +10,7 @@ * Regression test for: https://github.com/badlogic/pi-mono/issues/1022 */ -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; import { completeSimple, getEnvApiKey } from "../src/stream.js"; diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 7732eda4a..a872453c2 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -1,4 +1,4 @@ -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; import { complete } from "../src/stream.js"; diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index 915b35e66..6cad7ff1b 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -1,4 +1,4 @@ -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; import { complete } from "../src/stream.js"; diff --git a/packages/ai/test/validation.test.ts b/packages/ai/test/validation.test.ts index 60de63a6c..fd9d0164f 100644 --- a/packages/ai/test/validation.test.ts +++ b/packages/ai/test/validation.test.ts @@ -1,17 +1,41 @@ -import { Type } from "@sinclair/typebox"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import type { ToolCall } from "../src/types.js"; +import { Type } from "typebox"; +import { describe, expect, it } from "vitest"; +import type { Tool, ToolCall } from "../src/types.js"; import { validateToolArguments } from "../src/utils/validation.js"; -afterEach(() => { - vi.restoreAllMocks(); -}); +function createToolCallWithPlainSchema( + schema: Tool["parameters"], + value: unknown, +): { + tool: Tool; + toolCall: ToolCall; +} { + const tool: Tool = { + name: "echo", + description: "Echo tool", + parameters: { + type: "object", + properties: { + value: schema, + }, + required: ["value"], + } as Tool["parameters"], + }; + + const toolCall: ToolCall = { + type: "toolCall", + id: "tool-1", + name: "echo", + arguments: { value }, + }; + + return { tool, toolCall }; +} describe("validateToolArguments", () => { - it("falls back to raw arguments without writing to stderr when runtime code generation is blocked", () => { + it("still validates when Function constructor is unavailable", () => { const originalFunction = globalThis.Function; - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - const tool = { + const tool: Tool = { name: "echo", description: "Echo tool", parameters: Type.Object({ @@ -30,10 +54,63 @@ describe("validateToolArguments", () => { }) as unknown as FunctionConstructor; try { - expect(validateToolArguments(tool, toolCall)).toEqual(toolCall.arguments); - expect(errorSpy).not.toHaveBeenCalled(); + expect(validateToolArguments(tool, toolCall)).toEqual({ count: 42 }); } finally { globalThis.Function = originalFunction; } }); + + it("coerces serialized plain JSON schemas with AJV-compatible primitive rules", () => { + const passingCases: Array<{ + schema: Tool["parameters"]; + input: unknown; + expected: unknown; + }> = [ + { schema: { type: "number" } as Tool["parameters"], input: "42", expected: 42 }, + { schema: { type: "number" } as Tool["parameters"], input: true, expected: 1 }, + { schema: { type: "number" } as Tool["parameters"], input: null, expected: 0 }, + { schema: { type: "integer" } as Tool["parameters"], input: "42", expected: 42 }, + { schema: { type: "boolean" } as Tool["parameters"], input: "true", expected: true }, + { schema: { type: "boolean" } as Tool["parameters"], input: "false", expected: false }, + { schema: { type: "boolean" } as Tool["parameters"], input: 1, expected: true }, + { schema: { type: "boolean" } as Tool["parameters"], input: 0, expected: false }, + { schema: { type: "string" } as Tool["parameters"], input: null, expected: "" }, + { schema: { type: "string" } as Tool["parameters"], input: true, expected: "true" }, + { schema: { type: "null" } as Tool["parameters"], input: "", expected: null }, + { schema: { type: "null" } as Tool["parameters"], input: 0, expected: null }, + { schema: { type: "null" } as Tool["parameters"], input: false, expected: null }, + { + schema: { type: ["number", "string"] } as Tool["parameters"], + input: "1", + expected: "1", + }, + { + schema: { type: ["boolean", "number"] } as Tool["parameters"], + input: "1", + expected: 1, + }, + ]; + + for (const testCase of passingCases) { + const { tool, toolCall } = createToolCallWithPlainSchema(testCase.schema, testCase.input); + expect(validateToolArguments(tool, toolCall)).toEqual({ value: testCase.expected }); + } + }); + + it("rejects invalid coercions for serialized plain JSON schemas", () => { + const failingCases: Array<{ + schema: Tool["parameters"]; + input: unknown; + }> = [ + { schema: { type: "boolean" } as Tool["parameters"], input: "1" }, + { schema: { type: "boolean" } as Tool["parameters"], input: "0" }, + { schema: { type: "null" } as Tool["parameters"], input: "null" }, + { schema: { type: "integer" } as Tool["parameters"], input: "42.1" }, + ]; + + for (const testCase of failingCases) { + const { tool, toolCall } = createToolCallWithPlainSchema(testCase.schema, testCase.input); + expect(() => validateToolArguments(tool, toolCall)).toThrow("Validation failed"); + } + }); }); diff --git a/packages/coding-agent/examples/extensions/antigravity-image-gen.ts b/packages/coding-agent/examples/extensions/antigravity-image-gen.ts index f4533a06f..bdc44d118 100644 --- a/packages/coding-agent/examples/extensions/antigravity-image-gen.ts +++ b/packages/coding-agent/examples/extensions/antigravity-image-gen.ts @@ -31,7 +31,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { StringEnum } from "@mariozechner/pi-ai"; import { type ExtensionAPI, getAgentDir, withFileMutationQueue } from "@mariozechner/pi-coding-agent"; -import { type Static, Type } from "@sinclair/typebox"; +import { type Static, Type } from "typebox"; const PROVIDER = "google-antigravity"; diff --git a/packages/coding-agent/examples/extensions/dynamic-tools.ts b/packages/coding-agent/examples/extensions/dynamic-tools.ts index 5df443482..0d8b83591 100644 --- a/packages/coding-agent/examples/extensions/dynamic-tools.ts +++ b/packages/coding-agent/examples/extensions/dynamic-tools.ts @@ -8,7 +8,7 @@ */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; const ECHO_PARAMS = Type.Object({ message: Type.String({ description: "Message to echo" }), diff --git a/packages/coding-agent/examples/extensions/question.ts b/packages/coding-agent/examples/extensions/question.ts index f812d33d1..f23fc68ee 100644 --- a/packages/coding-agent/examples/extensions/question.ts +++ b/packages/coding-agent/examples/extensions/question.ts @@ -6,7 +6,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth } from "@mariozechner/pi-tui"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; interface OptionWithDesc { label: string; diff --git a/packages/coding-agent/examples/extensions/questionnaire.ts b/packages/coding-agent/examples/extensions/questionnaire.ts index 746f5a894..b56b659b0 100644 --- a/packages/coding-agent/examples/extensions/questionnaire.ts +++ b/packages/coding-agent/examples/extensions/questionnaire.ts @@ -7,7 +7,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth } from "@mariozechner/pi-tui"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; // Types interface QuestionOption { diff --git a/packages/coding-agent/examples/extensions/reload-runtime.ts b/packages/coding-agent/examples/extensions/reload-runtime.ts index e7b41e765..ecca96bd4 100644 --- a/packages/coding-agent/examples/extensions/reload-runtime.ts +++ b/packages/coding-agent/examples/extensions/reload-runtime.ts @@ -6,7 +6,7 @@ */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; export default function (pi: ExtensionAPI) { // Command entrypoint for reload. diff --git a/packages/coding-agent/examples/extensions/shutdown-command.ts b/packages/coding-agent/examples/extensions/shutdown-command.ts index b2243056c..bb570b161 100644 --- a/packages/coding-agent/examples/extensions/shutdown-command.ts +++ b/packages/coding-agent/examples/extensions/shutdown-command.ts @@ -6,7 +6,7 @@ */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; export default function (pi: ExtensionAPI) { // Register a /quit command that cleanly exits pi diff --git a/packages/coding-agent/examples/extensions/subagent/index.ts b/packages/coding-agent/examples/extensions/subagent/index.ts index ca65341d5..24c524d9c 100644 --- a/packages/coding-agent/examples/extensions/subagent/index.ts +++ b/packages/coding-agent/examples/extensions/subagent/index.ts @@ -21,7 +21,7 @@ import type { Message } from "@mariozechner/pi-ai"; import { StringEnum } from "@mariozechner/pi-ai"; import { type ExtensionAPI, getMarkdownTheme, withFileMutationQueue } from "@mariozechner/pi-coding-agent"; import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js"; const MAX_PARALLEL_TASKS = 8; diff --git a/packages/coding-agent/examples/extensions/tic-tac-toe.ts b/packages/coding-agent/examples/extensions/tic-tac-toe.ts index 8abcfe22d..78e0ad658 100644 --- a/packages/coding-agent/examples/extensions/tic-tac-toe.ts +++ b/packages/coding-agent/examples/extensions/tic-tac-toe.ts @@ -20,7 +20,7 @@ import { StringEnum } from "@mariozechner/pi-ai"; import type { ExtensionAPI, ExtensionContext, Theme, ToolExecutionMode } from "@mariozechner/pi-coding-agent"; import { type Component, matchesKey, Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; // Thrown from the tool on illegal actions. The agent runtime surfaces thrown // errors as tool errors (isError=true) without resetting any of our state. diff --git a/packages/coding-agent/examples/extensions/todo.ts b/packages/coding-agent/examples/extensions/todo.ts index 157499da3..47c84be90 100644 --- a/packages/coding-agent/examples/extensions/todo.ts +++ b/packages/coding-agent/examples/extensions/todo.ts @@ -13,7 +13,7 @@ import { StringEnum } from "@mariozechner/pi-ai"; import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent"; import { matchesKey, Text, truncateToWidth } from "@mariozechner/pi-tui"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; interface Todo { id: number; diff --git a/packages/coding-agent/examples/extensions/tool-override.ts b/packages/coding-agent/examples/extensions/tool-override.ts index 5ba3444a8..e8a924797 100644 --- a/packages/coding-agent/examples/extensions/tool-override.ts +++ b/packages/coding-agent/examples/extensions/tool-override.ts @@ -22,10 +22,10 @@ import type { TextContent } from "@mariozechner/pi-ai"; import { type ExtensionAPI, getAgentDir, withFileMutationQueue } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; import { constants, readFileSync } from "fs"; import { access, appendFile, readFile } from "fs/promises"; import { join, resolve } from "path"; +import { Type } from "typebox"; const LOG_FILE = join(getAgentDir(), "read-access.log"); diff --git a/packages/coding-agent/examples/extensions/truncated-tool.ts b/packages/coding-agent/examples/extensions/truncated-tool.ts index 4d11c6bf8..bbf78b673 100644 --- a/packages/coding-agent/examples/extensions/truncated-tool.ts +++ b/packages/coding-agent/examples/extensions/truncated-tool.ts @@ -25,10 +25,10 @@ import { withFileMutationQueue, } from "@mariozechner/pi-coding-agent"; import { Text } from "@mariozechner/pi-tui"; -import { Type } from "@sinclair/typebox"; import { execSync } from "child_process"; import { tmpdir } from "os"; import { join } from "path"; +import { Type } from "typebox"; const RgParams = Type.Object({ pattern: Type.String({ description: "Search pattern (regex)" }), diff --git a/packages/coding-agent/examples/extensions/with-deps/index.ts b/packages/coding-agent/examples/extensions/with-deps/index.ts index a352f6959..5413095c0 100644 --- a/packages/coding-agent/examples/extensions/with-deps/index.ts +++ b/packages/coding-agent/examples/extensions/with-deps/index.ts @@ -6,8 +6,8 @@ */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; import ms from "ms"; +import { Type } from "typebox"; export default function (pi: ExtensionAPI) { // Register a tool that uses ms diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index de5c2c438..826cb531b 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -44,8 +44,7 @@ "@mariozechner/pi-ai": "^0.68.1", "@mariozechner/pi-tui": "^0.68.1", "@silvia-odwyer/photon-node": "^0.3.4", - "@sinclair/typebox": "^0.34.41", - "ajv": "^8.17.1", + "typebox": "^1.1.24", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", "diff": "^8.0.2", diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 5981aed07..56688a6bc 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -18,7 +18,9 @@ import * as _bundledPiTui from "@mariozechner/pi-tui"; // Static imports of packages that extensions may use. // These MUST be static so Bun bundles them into the compiled binary. // The virtualModules option then makes them available to extensions. -import * as _bundledTypebox from "@sinclair/typebox"; +import * as _bundledTypebox from "typebox"; +import * as _bundledTypeboxCompile from "typebox/compile"; +import * as _bundledTypeboxValue from "typebox/value"; import { getAgentDir, isBunBinary } from "../../config.js"; // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts, // avoiding a circular dependency. Extensions can import from @mariozechner/pi-coding-agent. @@ -27,6 +29,7 @@ import { createEventBus, type EventBus } from "../event-bus.js"; import type { ExecOptions } from "../exec.js"; import { execCommand } from "../exec.js"; import { createSyntheticSourceInfo } from "../source-info.js"; +import * as _bundledTypeboxCompilerCompat from "./typebox-compiler-compat.js"; import type { Extension, ExtensionAPI, @@ -41,7 +44,14 @@ import type { /** Modules available to extensions via virtualModules (for compiled Bun binary) */ const VIRTUAL_MODULES: Record = { + typebox: _bundledTypebox, + "typebox/compile": _bundledTypeboxCompile, + "typebox/value": _bundledTypeboxValue, + "typebox/compiler": _bundledTypeboxCompilerCompat, "@sinclair/typebox": _bundledTypebox, + "@sinclair/typebox/compile": _bundledTypeboxCompile, + "@sinclair/typebox/value": _bundledTypeboxValue, + "@sinclair/typebox/compiler": _bundledTypeboxCompilerCompat, "@mariozechner/pi-agent-core": _bundledPiAgentCore, "@mariozechner/pi-tui": _bundledPiTui, "@mariozechner/pi-ai": _bundledPiAi, @@ -56,14 +66,25 @@ const require = createRequire(import.meta.url); * In Bun binary mode, virtualModules is used instead. */ let _aliases: Record | null = null; + +function resolveTypeboxCompilerCompatPath(baseDir: string): string { + const jsPath = path.resolve(baseDir, "typebox-compiler-compat.js"); + if (fs.existsSync(jsPath)) { + return jsPath; + } + return path.resolve(baseDir, "typebox-compiler-compat.ts"); +} + function getAliases(): Record { if (_aliases) return _aliases; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const packageIndex = path.resolve(__dirname, "../..", "index.js"); - const typeboxEntry = require.resolve("@sinclair/typebox"); - const typeboxRoot = typeboxEntry.replace(/[\\/]build[\\/]cjs[\\/]index\.js$/, ""); + const typeboxEntry = require.resolve("typebox"); + const typeboxCompileEntry = require.resolve("typebox/compile"); + const typeboxValueEntry = require.resolve("typebox/value"); + const typeboxCompilerCompat = resolveTypeboxCompilerCompatPath(__dirname); const packagesRoot = path.resolve(__dirname, "../../../../"); const resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string => { @@ -80,7 +101,14 @@ function getAliases(): Record { "@mariozechner/pi-tui": resolveWorkspaceOrImport("tui/dist/index.js", "@mariozechner/pi-tui"), "@mariozechner/pi-ai": resolveWorkspaceOrImport("ai/dist/index.js", "@mariozechner/pi-ai"), "@mariozechner/pi-ai/oauth": resolveWorkspaceOrImport("ai/dist/oauth.js", "@mariozechner/pi-ai/oauth"), - "@sinclair/typebox": typeboxRoot, + typebox: typeboxEntry, + "typebox/compile": typeboxCompileEntry, + "typebox/value": typeboxValueEntry, + "typebox/compiler": typeboxCompilerCompat, + "@sinclair/typebox": typeboxEntry, + "@sinclair/typebox/compile": typeboxCompileEntry, + "@sinclair/typebox/value": typeboxValueEntry, + "@sinclair/typebox/compiler": typeboxCompilerCompat, }; return _aliases; diff --git a/packages/coding-agent/src/core/extensions/typebox-compiler-compat.ts b/packages/coding-agent/src/core/extensions/typebox-compiler-compat.ts new file mode 100644 index 000000000..94bdcd24c --- /dev/null +++ b/packages/coding-agent/src/core/extensions/typebox-compiler-compat.ts @@ -0,0 +1,19 @@ +import { Code, Compile, Validator } from "typebox/compile"; + +export { Code, Compile, Validator }; + +// Legacy @sinclair/typebox/compiler compatibility for extensions. +export const TypeCompiler = { + Compile, +}; + +// In TypeBox 0.x this was a named export. Map it to the v1 Validator class. +export const TypeCheck = Validator; + +export default { + Code, + Compile, + Validator, + TypeCompiler, + TypeCheck, +}; diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 0ec391879..86ec071be 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -39,7 +39,7 @@ import type { OverlayOptions, TUI, } from "@mariozechner/pi-tui"; -import type { Static, TSchema } from "@sinclair/typebox"; +import type { Static, TSchema } from "typebox"; import type { Theme } from "../../modes/interactive/theme/theme.js"; import type { BashResult } from "../bash-executor.js"; import type { CompactionPreparation, CompactionResult } from "../compaction/index.js"; @@ -479,7 +479,7 @@ type AnyToolDefinition = ToolDefinition; export function defineTool( tool: ToolDefinition, ): ToolDefinition & AnyToolDefinition { - return tool; + return tool as ToolDefinition & AnyToolDefinition; } // ============================================================================ diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index fbfac4b3d..40f942558 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -18,10 +18,11 @@ import { type SimpleStreamOptions, } from "@mariozechner/pi-ai"; import { registerOAuthProvider, resetOAuthProviders } from "@mariozechner/pi-ai/oauth"; -import { type Static, Type } from "@sinclair/typebox"; -import AjvModule from "ajv"; import { existsSync, readFileSync } from "fs"; import { join } from "path"; +import { type Static, Type } from "typebox"; +import { Compile } from "typebox/compile"; +import type { TLocalizedValidationError } from "typebox/error"; import { getAgentDir } from "../config.js"; import type { AuthStorage } from "./auth-storage.js"; import { @@ -31,9 +32,6 @@ import { resolveHeadersOrThrow, } from "./resolve-config-value.js"; -const Ajv = (AjvModule as any).default || AjvModule; -const ajv = new Ajv(); - // Schema for OpenRouter routing preferences const PercentileCutoffsSchema = Type.Object({ p50: Type.Optional(Type.Number()), @@ -179,10 +177,23 @@ const ModelsConfigSchema = Type.Object({ providers: Type.Record(Type.String(), ProviderConfigSchema), }); -ajv.addSchema(ModelsConfigSchema, "ModelsConfig"); +const validateModelsConfig = Compile(ModelsConfigSchema); type ModelsConfig = Static; +function formatValidationPath(error: TLocalizedValidationError): string { + if (error.keyword === "required") { + const requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties; + const requiredProperty = requiredProperties?.[0]; + if (requiredProperty) { + const basePath = error.instancePath.replace(/^\//, "").replace(/\//g, "."); + return basePath ? `${basePath}.${requiredProperty}` : requiredProperty; + } + } + const path = error.instancePath.replace(/^\//, "").replace(/\//g, "."); + return path || "root"; +} + /** Provider override config (baseUrl, compat) without request auth/headers */ interface ProviderOverride { baseUrl?: string; @@ -417,17 +428,19 @@ export class ModelRegistry { try { const content = readFileSync(modelsJsonPath, "utf-8"); - const config: ModelsConfig = JSON.parse(content); + const parsed = JSON.parse(content) as unknown; - // Validate schema - const validate = ajv.getSchema("ModelsConfig")!; - if (!validate(config)) { + if (!validateModelsConfig.Check(parsed)) { const errors = - validate.errors?.map((e: any) => ` - ${e.instancePath || "root"}: ${e.message}`).join("\n") || - "Unknown schema error"; + validateModelsConfig + .Errors(parsed) + .map((error) => ` - ${formatValidationPath(error)}: ${error.message}`) + .join("\n") || "Unknown schema error"; return emptyCustomModelsResult(`Invalid models.json schema:\n${errors}\n\nFile: ${modelsJsonPath}`); } + const config = parsed as ModelsConfig; + // Additional validation this.validateConfig(config); diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 736ff66ec..b6146117b 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -4,8 +4,8 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import { Container, Text, truncateToWidth } from "@mariozechner/pi-tui"; -import { type Static, Type } from "@sinclair/typebox"; import { spawn } from "child_process"; +import { type Static, Type } from "typebox"; import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js"; import { theme } from "../../modes/interactive/theme/theme.js"; diff --git a/packages/coding-agent/src/core/tools/edit.ts b/packages/coding-agent/src/core/tools/edit.ts index 44a38ad02..24106544a 100644 --- a/packages/coding-agent/src/core/tools/edit.ts +++ b/packages/coding-agent/src/core/tools/edit.ts @@ -1,8 +1,8 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import { Box, Container, Spacer, Text } from "@mariozechner/pi-tui"; -import { type Static, Type } from "@sinclair/typebox"; import { constants } from "fs"; import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises"; +import { type Static, Type } from "typebox"; import { renderDiff } from "../../modes/interactive/components/diff.js"; import type { ToolDefinition } from "../extensions/types.js"; import { diff --git a/packages/coding-agent/src/core/tools/find.ts b/packages/coding-agent/src/core/tools/find.ts index 4ca5e2c54..ed2c8fcf6 100644 --- a/packages/coding-agent/src/core/tools/find.ts +++ b/packages/coding-agent/src/core/tools/find.ts @@ -1,10 +1,10 @@ import { createInterface } from "node:readline"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import { Text } from "@mariozechner/pi-tui"; -import { type Static, Type } from "@sinclair/typebox"; import { spawn } from "child_process"; import { existsSync } from "fs"; import path from "path"; +import { type Static, Type } from "typebox"; import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; import { ensureTool } from "../../utils/tools-manager.js"; import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; diff --git a/packages/coding-agent/src/core/tools/grep.ts b/packages/coding-agent/src/core/tools/grep.ts index 46928c392..eb2b4d9a5 100644 --- a/packages/coding-agent/src/core/tools/grep.ts +++ b/packages/coding-agent/src/core/tools/grep.ts @@ -1,10 +1,10 @@ import { createInterface } from "node:readline"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import { Text } from "@mariozechner/pi-tui"; -import { type Static, Type } from "@sinclair/typebox"; import { spawn } from "child_process"; import { readFileSync, statSync } from "fs"; import path from "path"; +import { type Static, Type } from "typebox"; import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; import { ensureTool } from "../../utils/tools-manager.js"; import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; diff --git a/packages/coding-agent/src/core/tools/ls.ts b/packages/coding-agent/src/core/tools/ls.ts index cb903250c..460f75fef 100644 --- a/packages/coding-agent/src/core/tools/ls.ts +++ b/packages/coding-agent/src/core/tools/ls.ts @@ -1,8 +1,8 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import { Text } from "@mariozechner/pi-tui"; -import { type Static, Type } from "@sinclair/typebox"; import { existsSync, readdirSync, statSync } from "fs"; import nodePath from "path"; +import { type Static, Type } from "typebox"; import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; import { resolveToCwd } from "./path-utils.js"; diff --git a/packages/coding-agent/src/core/tools/read.ts b/packages/coding-agent/src/core/tools/read.ts index 26095550c..8f8e4656c 100644 --- a/packages/coding-agent/src/core/tools/read.ts +++ b/packages/coding-agent/src/core/tools/read.ts @@ -1,9 +1,9 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { Api, ImageContent, Model, TextContent } from "@mariozechner/pi-ai"; import { Text } from "@mariozechner/pi-tui"; -import { type Static, Type } from "@sinclair/typebox"; import { constants } from "fs"; import { access as fsAccess, readFile as fsReadFile } from "fs/promises"; +import { type Static, Type } from "typebox"; import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js"; import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js"; diff --git a/packages/coding-agent/src/core/tools/write.ts b/packages/coding-agent/src/core/tools/write.ts index af07bdda5..e8eda9426 100644 --- a/packages/coding-agent/src/core/tools/write.ts +++ b/packages/coding-agent/src/core/tools/write.ts @@ -1,8 +1,8 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import { Container, Text } from "@mariozechner/pi-tui"; -import { type Static, Type } from "@sinclair/typebox"; import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises"; import { dirname } from "path"; +import { type Static, Type } from "typebox"; import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js"; import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 40f717cd0..5159315b0 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -1,10 +1,10 @@ import * as fs from "node:fs"; import * as path from "node:path"; import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@mariozechner/pi-tui"; -import { type Static, Type } from "@sinclair/typebox"; -import { TypeCompiler } from "@sinclair/typebox/compiler"; import chalk from "chalk"; import { highlight, supportsLanguage } from "cli-highlight"; +import { type Static, Type } from "typebox"; +import { Compile } from "typebox/compile"; import { getCustomThemesDir, getThemesDir } from "../../../config.js"; import type { SourceInfo } from "../../../core/source-info.js"; @@ -94,7 +94,7 @@ const ThemeJsonSchema = Type.Object({ type ThemeJson = Static; -const validateThemeJson = TypeCompiler.Compile(ThemeJsonSchema); +const validateThemeJson = Compile(ThemeJsonSchema); export type ThemeColor = | "accent" @@ -515,23 +515,29 @@ export function getAvailableThemesWithPaths(): ThemeInfo[] { function parseThemeJson(label: string, json: unknown): ThemeJson { if (!validateThemeJson.Check(json)) { const errors = Array.from(validateThemeJson.Errors(json)); - const missingColors: string[] = []; + const missingColors = new Set(); const otherErrors: string[] = []; - for (const e of errors) { - // Check for missing required color properties - const match = e.path.match(/^\/colors\/(\w+)$/); - if (match && e.message.includes("Required")) { - missingColors.push(match[1]); - } else { - otherErrors.push(` - ${e.path}: ${e.message}`); + for (const error of errors) { + if (error.keyword === "required" && error.instancePath === "/colors") { + const requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties; + for (const requiredProperty of requiredProperties ?? []) { + missingColors.add(requiredProperty); + } + continue; } + + const path = error.instancePath || "/"; + otherErrors.push(` - ${path}: ${error.message}`); } let errorMessage = `Invalid theme "${label}":\n`; - if (missingColors.length > 0) { + if (missingColors.size > 0) { errorMessage += "\nMissing required color tokens:\n"; - errorMessage += missingColors.map((c) => ` - ${c}`).join("\n"); + errorMessage += Array.from(missingColors) + .sort() + .map((color) => ` - ${color}`) + .join("\n"); errorMessage += '\n\nPlease add these colors to your theme\'s "colors" object.'; errorMessage += "\nSee the built-in themes (dark.json, light.json) for reference values."; } diff --git a/packages/coding-agent/test/agent-session-concurrent.test.ts b/packages/coding-agent/test/agent-session-concurrent.test.ts index 28001fbda..bd3d71247 100644 --- a/packages/coding-agent/test/agent-session-concurrent.test.ts +++ b/packages/coding-agent/test/agent-session-concurrent.test.ts @@ -14,7 +14,7 @@ import { type ImageContent, type TextContent, } from "@mariozechner/pi-ai"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AgentSession } from "../src/core/agent-session.js"; import { AuthStorage } from "../src/core/auth-storage.js"; diff --git a/packages/coding-agent/test/agent-session-dynamic-tools.test.ts b/packages/coding-agent/test/agent-session-dynamic-tools.test.ts index 496b50caa..c31c8e265 100644 --- a/packages/coding-agent/test/agent-session-dynamic-tools.test.ts +++ b/packages/coding-agent/test/agent-session-dynamic-tools.test.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { getModel } from "@mariozechner/pi-ai"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { DefaultResourceLoader } from "../src/core/resource-loader.js"; import { createAgentSession } from "../src/core/sdk.js"; diff --git a/packages/coding-agent/test/agent-session-retry.test.ts b/packages/coding-agent/test/agent-session-retry.test.ts index bedeb81ac..4c4a69ac8 100644 --- a/packages/coding-agent/test/agent-session-retry.test.ts +++ b/packages/coding-agent/test/agent-session-retry.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent, type AgentEvent, type AgentTool } from "@mariozechner/pi-agent-core"; import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@mariozechner/pi-ai"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AgentSession } from "../src/core/agent-session.js"; import { AuthStorage } from "../src/core/auth-storage.js"; diff --git a/packages/coding-agent/test/extensions-discovery.test.ts b/packages/coding-agent/test/extensions-discovery.test.ts index a6955bfc4..52029d2cf 100644 --- a/packages/coding-agent/test/extensions-discovery.test.ts +++ b/packages/coding-agent/test/extensions-discovery.test.ts @@ -28,7 +28,7 @@ describe("extensions discovery", () => { `; const extensionCodeWithTool = (toolName: string) => ` - import { Type } from "@sinclair/typebox"; + import { Type } from "typebox"; export default function(pi) { pi.registerTool({ name: "${toolName}", diff --git a/packages/coding-agent/test/extensions-runner.test.ts b/packages/coding-agent/test/extensions-runner.test.ts index 8ad5607b7..67c462358 100644 --- a/packages/coding-agent/test/extensions-runner.test.ts +++ b/packages/coding-agent/test/extensions-runner.test.ts @@ -291,7 +291,7 @@ describe("ExtensionRunner", () => { describe("tool collection", () => { it("collects tools from multiple extensions", async () => { const toolCode = (name: string) => ` - import { Type } from "@sinclair/typebox"; + import { Type } from "typebox"; export default function(pi) { pi.registerTool({ name: "${name}", @@ -315,7 +315,7 @@ describe("ExtensionRunner", () => { it("keeps first tool when two extensions register the same name", async () => { const first = ` - import { Type } from "@sinclair/typebox"; + import { Type } from "typebox"; export default function(pi) { pi.registerTool({ name: "shared", @@ -327,7 +327,7 @@ describe("ExtensionRunner", () => { } `; const second = ` - import { Type } from "@sinclair/typebox"; + import { Type } from "typebox"; export default function(pi) { pi.registerTool({ name: "shared", diff --git a/packages/coding-agent/test/resource-loader.test.ts b/packages/coding-agent/test/resource-loader.test.ts index 054b7da67..8d9999e1e 100644 --- a/packages/coding-agent/test/resource-loader.test.ts +++ b/packages/coding-agent/test/resource-loader.test.ts @@ -471,7 +471,7 @@ Content`, join(ext1Dir, "index.ts"), ` import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; export default function(pi: ExtensionAPI) { pi.registerTool({ name: "duplicate-tool", @@ -486,7 +486,7 @@ export default function(pi: ExtensionAPI) { join(ext2Dir, "index.ts"), ` import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; export default function(pi: ExtensionAPI) { pi.registerTool({ name: "duplicate-tool", @@ -513,7 +513,7 @@ export default function(pi: ExtensionAPI) { join(globalExtDir, "global.ts"), ` import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; export default function(pi: ExtensionAPI) { pi.registerTool({ name: "duplicate-tool", @@ -532,7 +532,7 @@ export default function(pi: ExtensionAPI) { explicitExtPath, ` import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; export default function(pi: ExtensionAPI) { pi.registerTool({ name: "duplicate-tool", diff --git a/packages/coding-agent/test/stdout-cleanliness.test.ts b/packages/coding-agent/test/stdout-cleanliness.test.ts index a0a787654..f9455f89c 100644 --- a/packages/coding-agent/test/stdout-cleanliness.test.ts +++ b/packages/coding-agent/test/stdout-cleanliness.test.ts @@ -60,6 +60,7 @@ async function runCli(args: string[]): Promise<{ stdout: string; stderr: string; env: { ...process.env, [ENV_AGENT_DIR]: agentDir, + TSX_TSCONFIG_PATH: resolve(__dirname, "../../../tsconfig.json"), }, stdio: ["ignore", "pipe", "pipe"], }); diff --git a/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts b/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts index f89030447..389989b47 100644 --- a/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts +++ b/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts @@ -1,7 +1,7 @@ import { Buffer } from "node:buffer"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import { fauxAssistantMessage, fauxToolCall } from "@mariozechner/pi-ai"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; import type { BashOperations } from "../../src/core/tools/bash.js"; import { createHarness, type Harness } from "./harness.js"; diff --git a/packages/coding-agent/test/suite/agent-session-model-extension.test.ts b/packages/coding-agent/test/suite/agent-session-model-extension.test.ts index 652093aa6..a30783529 100644 --- a/packages/coding-agent/test/suite/agent-session-model-extension.test.ts +++ b/packages/coding-agent/test/suite/agent-session-model-extension.test.ts @@ -1,6 +1,6 @@ import type { AgentTool, ThinkingLevel } from "@mariozechner/pi-agent-core"; import { fauxAssistantMessage, fauxToolCall, type Model } from "@mariozechner/pi-ai"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; import type { ExtensionAPI } from "../../src/index.js"; import { createHarness, getAssistantTexts, type Harness } from "./harness.js"; diff --git a/packages/coding-agent/test/suite/agent-session-prompt.test.ts b/packages/coding-agent/test/suite/agent-session-prompt.test.ts index aaeb58964..059739c97 100644 --- a/packages/coding-agent/test/suite/agent-session-prompt.test.ts +++ b/packages/coding-agent/test/suite/agent-session-prompt.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import { fauxAssistantMessage, fauxToolCall, type Model } from "@mariozechner/pi-ai"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; import type { PromptTemplate } from "../../src/core/prompt-templates.js"; import { createSyntheticSourceInfo } from "../../src/core/source-info.js"; diff --git a/packages/coding-agent/test/suite/agent-session-queue.test.ts b/packages/coding-agent/test/suite/agent-session-queue.test.ts index a46cce6f8..f62169381 100644 --- a/packages/coding-agent/test/suite/agent-session-queue.test.ts +++ b/packages/coding-agent/test/suite/agent-session-queue.test.ts @@ -1,7 +1,7 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import { fauxAssistantMessage, fauxToolCall } from "@mariozechner/pi-ai"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; import { createHarness, getAssistantTexts, getMessageText, getUserTexts, type Harness } from "./harness.js"; diff --git a/packages/coding-agent/test/suite/agent-session-retry-events.test.ts b/packages/coding-agent/test/suite/agent-session-retry-events.test.ts index 7da3c07ef..806d9c766 100644 --- a/packages/coding-agent/test/suite/agent-session-retry-events.test.ts +++ b/packages/coding-agent/test/suite/agent-session-retry-events.test.ts @@ -1,6 +1,6 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import { fauxAssistantMessage, fauxThinking, fauxToolCall } from "@mariozechner/pi-ai"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; import { createHarness, type Harness } from "./harness.js"; diff --git a/packages/coding-agent/test/suite/regressions/2023-queued-slash-command-followup.test.ts b/packages/coding-agent/test/suite/regressions/2023-queued-slash-command-followup.test.ts index 36dfccd01..080e3d4a7 100644 --- a/packages/coding-agent/test/suite/regressions/2023-queued-slash-command-followup.test.ts +++ b/packages/coding-agent/test/suite/regressions/2023-queued-slash-command-followup.test.ts @@ -1,7 +1,7 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import { fauxAssistantMessage, fauxToolCall } from "@mariozechner/pi-ai"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; import { createHarness, getAssistantTexts, getUserTexts, type Harness } from "../harness.js"; diff --git a/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts b/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts index 333bbf06a..358928362 100644 --- a/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts +++ b/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { getModel } from "@mariozechner/pi-ai"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { DefaultResourceLoader } from "../../../src/core/resource-loader.js"; import { createAgentSession } from "../../../src/core/sdk.js"; diff --git a/packages/coding-agent/test/test-harness.test.ts b/packages/coding-agent/test/test-harness.test.ts index 71589194d..7685cc0fd 100644 --- a/packages/coding-agent/test/test-harness.test.ts +++ b/packages/coding-agent/test/test-harness.test.ts @@ -5,7 +5,7 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { AssistantMessage } from "@mariozechner/pi-ai"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; import { createHarness, createHarnessWithExtensions, type Harness } from "./test-harness.js"; diff --git a/packages/coding-agent/test/tool-execution-component.test.ts b/packages/coding-agent/test/tool-execution-component.test.ts index b9b597eb4..04a9f7a9b 100644 --- a/packages/coding-agent/test/tool-execution-component.test.ts +++ b/packages/coding-agent/test/tool-execution-component.test.ts @@ -1,6 +1,6 @@ import { Text, type TUI } from "@mariozechner/pi-tui"; -import { Type } from "@sinclair/typebox"; import stripAnsi from "strip-ansi"; +import { Type } from "typebox"; import { beforeAll, describe, expect, test } from "vitest"; import type { ToolDefinition } from "../src/core/extensions/types.js"; import { type BashOperations, createBashToolDefinition } from "../src/core/tools/bash.js"; diff --git a/packages/coding-agent/tsconfig.examples.json b/packages/coding-agent/tsconfig.examples.json index 6f9cb5f64..28fdaa293 100644 --- a/packages/coding-agent/tsconfig.examples.json +++ b/packages/coding-agent/tsconfig.examples.json @@ -7,7 +7,7 @@ "@mariozechner/pi-coding-agent/hooks": ["./src/core/hooks/index.ts"], "@mariozechner/pi-tui": ["../tui/src/index.ts"], "@mariozechner/pi-ai": ["../ai/src/index.ts"], - "@sinclair/typebox": ["../../node_modules/@sinclair/typebox"] + "typebox": ["../../node_modules/typebox"] }, "skipLibCheck": true }, diff --git a/packages/coding-agent/vitest.config.ts b/packages/coding-agent/vitest.config.ts index eaf372110..164b7e7d0 100644 --- a/packages/coding-agent/vitest.config.ts +++ b/packages/coding-agent/vitest.config.ts @@ -1,14 +1,26 @@ -import { defineConfig } from 'vitest/config'; +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vitest/config"; + +const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url)); +const aiSrcOAuth = fileURLToPath(new URL("../ai/src/oauth.ts", import.meta.url)); +const agentSrcIndex = fileURLToPath(new URL("../agent/src/index.ts", import.meta.url)); export default defineConfig({ - test: { - globals: true, - environment: 'node', - testTimeout: 30000, // 30 seconds for API calls - server: { - deps: { - external: [/@silvia-odwyer\/photon-node/], - }, - }, - }, + test: { + globals: true, + environment: "node", + testTimeout: 30000, + server: { + deps: { + external: [/@silvia-odwyer\/photon-node/], + }, + }, + }, + resolve: { + alias: [ + { find: /^@mariozechner\/pi-ai$/, replacement: aiSrcIndex }, + { find: /^@mariozechner\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, + { find: /^@mariozechner\/pi-agent-core$/, replacement: agentSrcIndex }, + ], + }, }); diff --git a/packages/mom/package.json b/packages/mom/package.json index 2832c1a87..53a40c2c7 100644 --- a/packages/mom/package.json +++ b/packages/mom/package.json @@ -23,7 +23,7 @@ "@mariozechner/pi-agent-core": "^0.68.1", "@mariozechner/pi-ai": "^0.68.1", "@mariozechner/pi-coding-agent": "^0.68.1", - "@sinclair/typebox": "^0.34.0", + "typebox": "^1.1.24", "@slack/socket-mode": "^2.0.0", "@slack/web-api": "^7.0.0", "chalk": "^5.6.2", diff --git a/packages/mom/src/tools/attach.ts b/packages/mom/src/tools/attach.ts index fae9e8db2..c9ff29cf0 100644 --- a/packages/mom/src/tools/attach.ts +++ b/packages/mom/src/tools/attach.ts @@ -1,6 +1,6 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; -import { Type } from "@sinclair/typebox"; import { basename, resolve as resolvePath } from "path"; +import { Type } from "typebox"; // This will be set by the agent before running let uploadFn: ((filePath: string, title?: string) => Promise) | null = null; diff --git a/packages/mom/src/tools/bash.ts b/packages/mom/src/tools/bash.ts index 82e9dacd8..7579a062d 100644 --- a/packages/mom/src/tools/bash.ts +++ b/packages/mom/src/tools/bash.ts @@ -3,7 +3,7 @@ import { createWriteStream } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { AgentTool } from "@mariozechner/pi-agent-core"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import type { Executor } from "../sandbox.js"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from "./truncate.js"; diff --git a/packages/mom/src/tools/edit.ts b/packages/mom/src/tools/edit.ts index 5ee678e8a..d113a9918 100644 --- a/packages/mom/src/tools/edit.ts +++ b/packages/mom/src/tools/edit.ts @@ -1,6 +1,6 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; -import { Type } from "@sinclair/typebox"; import * as Diff from "diff"; +import { Type } from "typebox"; import type { Executor } from "../sandbox.js"; /** diff --git a/packages/mom/src/tools/read.ts b/packages/mom/src/tools/read.ts index 4f284d708..5ba83edef 100644 --- a/packages/mom/src/tools/read.ts +++ b/packages/mom/src/tools/read.ts @@ -1,7 +1,7 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { ImageContent, TextContent } from "@mariozechner/pi-ai"; -import { Type } from "@sinclair/typebox"; import { extname } from "path"; +import { Type } from "typebox"; import type { Executor } from "../sandbox.js"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js"; diff --git a/packages/mom/src/tools/write.ts b/packages/mom/src/tools/write.ts index ebd0735bd..3e807c6f8 100644 --- a/packages/mom/src/tools/write.ts +++ b/packages/mom/src/tools/write.ts @@ -1,5 +1,5 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; -import { Type } from "@sinclair/typebox"; +import { Type } from "typebox"; import type { Executor } from "../sandbox.js"; const writeSchema = Type.Object({ diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index 992f774d0..f41555096 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -20,6 +20,7 @@ "@lmstudio/sdk": "^1.5.0", "@mariozechner/pi-ai": "^0.68.1", "@mariozechner/pi-tui": "^0.68.1", + "typebox": "^1.1.24", "docx-preview": "^0.3.7", "jszip": "^3.10.1", "lucide": "^0.544.0", diff --git a/packages/web-ui/src/tools/artifacts/artifacts.ts b/packages/web-ui/src/tools/artifacts/artifacts.ts index 0a5474a7a..041a96022 100644 --- a/packages/web-ui/src/tools/artifacts/artifacts.ts +++ b/packages/web-ui/src/tools/artifacts/artifacts.ts @@ -3,11 +3,11 @@ import "@mariozechner/mini-lit/dist/MarkdownBlock.js"; import { Button } from "@mariozechner/mini-lit/dist/Button.js"; import type { Agent, AgentMessage, AgentTool } from "@mariozechner/pi-agent-core"; import { StringEnum, type ToolCall } from "@mariozechner/pi-ai"; -import { type Static, Type } from "@sinclair/typebox"; import { html, LitElement, type TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { createRef, type Ref, ref } from "lit/directives/ref.js"; import { X } from "lucide"; +import { type Static, Type } from "typebox"; import type { ArtifactMessage } from "../../components/Messages.js"; import { ArtifactsRuntimeProvider } from "../../components/sandbox/ArtifactsRuntimeProvider.js"; import { AttachmentsRuntimeProvider } from "../../components/sandbox/AttachmentsRuntimeProvider.js"; diff --git a/packages/web-ui/src/tools/extract-document.ts b/packages/web-ui/src/tools/extract-document.ts index b733c7eea..8dad2b4dc 100644 --- a/packages/web-ui/src/tools/extract-document.ts +++ b/packages/web-ui/src/tools/extract-document.ts @@ -1,9 +1,9 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { ToolResultMessage } from "@mariozechner/pi-ai"; -import { type Static, Type } from "@sinclair/typebox"; import { html } from "lit"; import { createRef, ref } from "lit/directives/ref.js"; import { FileText } from "lucide"; +import { type Static, Type } from "typebox"; import { EXTRACT_DOCUMENT_DESCRIPTION } from "../prompts/prompts.js"; import { loadAttachment } from "../utils/attachment-utils.js"; import { isCorsError } from "../utils/proxy-utils.js"; diff --git a/packages/web-ui/src/tools/javascript-repl.ts b/packages/web-ui/src/tools/javascript-repl.ts index c42ed9e7a..c3358aca9 100644 --- a/packages/web-ui/src/tools/javascript-repl.ts +++ b/packages/web-ui/src/tools/javascript-repl.ts @@ -1,10 +1,10 @@ import { i18n } from "@mariozechner/mini-lit"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { ToolResultMessage } from "@mariozechner/pi-ai"; -import { type Static, Type } from "@sinclair/typebox"; import { html } from "lit"; import { createRef, ref } from "lit/directives/ref.js"; import { Code } from "lucide"; +import { type Static, Type } from "typebox"; import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.js"; import type { SandboxRuntimeProvider } from "../components/sandbox/SandboxRuntimeProvider.js"; import { JAVASCRIPT_REPL_TOOL_DESCRIPTION } from "../prompts/prompts.js"; diff --git a/tsconfig.json b/tsconfig.json index 079f788fc..815d247a7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "@mariozechner/pi-coding-agent": ["./packages/coding-agent/src/index.ts"], "@mariozechner/pi-coding-agent/hooks": ["./packages/coding-agent/src/core/hooks/index.ts"], "@mariozechner/pi-coding-agent/*": ["./packages/coding-agent/src/*"], - "@sinclair/typebox": ["./node_modules/@sinclair/typebox"], + "typebox": ["./node_modules/typebox"], "@mariozechner/pi-mom": ["./packages/mom/src/index.ts"], "@mariozechner/pi-mom/*": ["./packages/mom/src/*"], "@mariozechner/pi": ["./packages/pods/src/index.ts"],