From 906143e9fd9c6646f8fb9c3b14d15bea3459f67a Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 18 Jun 2026 01:29:32 +0000 Subject: [PATCH 1/3] fix(cli): restored legacy extension shim exports Added compatibility exports for legacy plugin validation, including defineTool, StringEnum, TypeBox bare imports, frontmatter helpers, SettingsManager, and createCodingTools. Updated SDK settingsManager alias handling and regression coverage for the affected shim surfaces. Fixes #2858 --- packages/coding-agent/CHANGELOG.md | 4 ++ .../src/extensibility/legacy-pi-ai-shim.ts | 22 +++++-- .../legacy-pi-coding-agent-shim.ts | 43 +++++++++++++ .../extensibility/plugins/legacy-pi-compat.ts | 26 ++++---- packages/coding-agent/src/sdk.ts | 9 ++- .../legacy-pi-ai-type-remap.test.ts | 63 +++++++++++++++++++ .../test/extensibility/typebox-remap.test.ts | 21 ++++++- 7 files changed, 168 insertions(+), 20 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8f7751577..81b168f46 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed legacy plugin validation for extensions that import `defineTool`, `StringEnum`, frontmatter helpers, `SettingsManager`, `createCodingTools`, or the bare `typebox` package through the hosted Pi compatibility shims ([#2858](https://github.com/can1357/oh-my-pi/issues/2858)). + ## [16.0.4] - 2026-06-17 ### Fixed diff --git a/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts b/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts index 8006e475b..18c0ac1e7 100644 --- a/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts +++ b/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts @@ -11,14 +11,26 @@ * bare package root. Subpath imports (`@oh-my-pi/pi-ai/oauth`, etc.) * continue to resolve directly against the bundled pi-ai package. * - * The `Type` runtime is borrowed from the Zod-backed TypeBox shim that - * already serves bare `@sinclair/typebox` imports for the same extension - * class, keeping the legacy-compat surface internally consistent. + * The `Type` runtime and legacy `StringEnum()` helper are borrowed from the + * Zod-backed TypeBox shim that already serves TypeBox imports for the same + * extension class, keeping the legacy-compat surface internally consistent. * * Type-level `Static` and `TSchema` continue to come from pi-ai's own * `types.ts` via the `export *` below — pi-ai still exports both as types, - * only the runtime `Type` builder was removed. + * only the runtime `Type` builder and `StringEnum()` helper were removed. */ +import { type TSchema, Type } from "./typebox"; + +export interface StringEnumOptions { + description?: string; + default?: T; + examples?: T[]; + [key: string]: unknown; +} + +export function StringEnum(values: readonly T[], options?: StringEnumOptions): TSchema { + return Type.Enum(values, options); +} export * from "@oh-my-pi/pi-ai"; -export { Type } from "./typebox"; +export { Type }; diff --git a/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts b/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts index 8eb32d814..1cb8104d4 100644 --- a/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts +++ b/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts @@ -12,4 +12,47 @@ * the same module identity as a direct `@oh-my-pi/pi-coding-agent` import. */ +import type { TSchema } from "@oh-my-pi/pi-ai"; +import { parseFrontmatter as parseOmpFrontmatter } from "@oh-my-pi/pi-utils"; +import { Settings } from "../config/settings"; +import type { ToolDefinition } from "./extensions/types"; +import { Type } from "./typebox"; + +export interface ParsedFrontmatter = Record> { + frontmatter: T; + body: string; +} + +export function parseFrontmatter = Record>( + content: string, +): ParsedFrontmatter { + const { frontmatter, body } = parseOmpFrontmatter(content, { level: "fatal" }); + return { frontmatter: frontmatter as T, body }; +} + +export function stripFrontmatter(content: string): string { + return parseFrontmatter(content).body; +} + +export function defineTool( + tool: ToolDefinition, +): ToolDefinition { + return tool; +} + +export function createCodingTools(_cwd: string): ToolDefinition[] { + return []; +} + +export const SettingsManager = { + create(cwd: string, agentDir?: string): Promise { + return Settings.init({ cwd, agentDir }); + }, + + inMemory(): Settings { + return Settings.isolated(); + }, +} as const; + export * from "../index"; +export { Type }; diff --git a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts index 49b4a0026..dd024c390 100644 --- a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts +++ b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts @@ -64,14 +64,14 @@ const packageRootCache = new Map(); const packageImportsCache = new Map | null>(); const PACKAGE_IMPORT_EXCLUDED = Symbol("packageImportExcluded"); -// Extensions that imported `@sinclair/typebox` directly used to resolve against a -// real `@sinclair/typebox` install. The runtime dep was replaced with the Zod-backed -// shim under `extensibility/typebox.ts`; plugins still importing the public name -// are redirected to that shim so existing extensions keep working without code -// changes. Submodules like `@sinclair/typebox/compiler` are intentionally not -// remapped — those expose TypeBox-only APIs the shim does not provide and plugins -// relying on them must vendor `@sinclair/typebox` directly. -const TYPEBOX_SPECIFIER_FILTER = /^@sinclair\/typebox$/; +// Extensions that imported TypeBox directly used to resolve against a real +// `@sinclair/typebox` or `typebox` install. The runtime dep was replaced with +// the Zod-backed shim under `extensibility/typebox.ts`; plugins still importing +// either public name are redirected to that shim so existing extensions keep +// working without code changes. Submodules like `@sinclair/typebox/compiler` +// are intentionally not remapped — those expose TypeBox-only APIs the shim does +// not provide and plugins relying on them must vendor TypeBox directly. +const TYPEBOX_SPECIFIER_FILTER = /^(?:@sinclair\/typebox|typebox)$/; // Compat shim and bundled-package paths used in compiled-binary mode. The shim // paths must point at files that ship inside the bunfs root; in dev / @@ -295,14 +295,14 @@ function rewriteLegacyPiImports(source: string): string { ); } -// Match the bare `@sinclair/typebox` import specifier (static + dynamic). -// Subpath imports like `@sinclair/typebox/compiler` are intentionally excluded — -// they expose TypeBox-only APIs the Zod-backed shim does not provide. -const TYPEBOX_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])(@sinclair\/typebox)(["'])/g; +// Match the bare TypeBox import specifiers (static + dynamic). Subpath imports +// like `@sinclair/typebox/compiler` are intentionally excluded — they expose +// TypeBox-only APIs the Zod-backed shim does not provide. +const TYPEBOX_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])(@sinclair\/typebox|typebox)(["'])/g; /** * Rewrite the extension-owned specifiers OMP must host-resolve — legacy - * `@(scope)/pi-*`, bare `@sinclair/typebox`, and package `imports` aliases like + * `@(scope)/pi-*`, bare TypeBox packages, and package `imports` aliases like * `#src/*` — to absolute `file://` URLs. Every other specifier (relative * siblings and third-party dependencies) is left untouched so Bun resolves it * natively from the extension's real on-disk location. diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index f322caaa4..1e6b07235 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -526,6 +526,11 @@ export interface CreateAgentSessionOptions { /** Settings instance. Default: Settings.init({ cwd, agentDir }) */ settings?: Settings; + /** + * Legacy alias for `settings`. Older Pi extensions pass SettingsManager.create(...) + * through this field; accept it so their SDK calls keep the configured settings. + */ + settingsManager?: Settings | Promise; /** Whether UI is available (enables interactive tools like ask). Default: false */ hasUI?: boolean; @@ -1131,7 +1136,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} startupCredentialDisabledEvents.push(event); } }); - const settings = options.settings ?? (await logger.time("settings", Settings.init, { cwd, agentDir })); + const settings = await (options.settings ?? + options.settingsManager ?? + logger.time("settings", Settings.init, { cwd, agentDir })); logger.time("initializeWithSettings", initializeWithSettings, settings); if (!options.modelRegistry) { modelRegistry.refreshInBackground(); diff --git a/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts b/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts index f56a0d305..6f0ebf583 100644 --- a/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts +++ b/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts @@ -88,6 +88,21 @@ describe("legacy-pi @(scope)/pi-ai root `Type` remap (issue #1437)", () => { expect(loaded.zodObj.safeParse({ name: "ok" }).success).toBe(true); expect(loaded.zodObj.safeParse({}).success).toBe(false); }); + it("preserves legacy StringEnum root imports as TypeBox-compatible schemas", async () => { + const entry = await writeFixtureExtension( + [ + 'import { StringEnum } from "@earendil-works/pi-ai";', + 'export const schema = StringEnum(["upstream", "downstream"] as const, { default: "upstream" });', + ].join("\n"), + ); + + const loaded = (await loadLegacyPiModule(entry)) as { + schema: { safeParse: (input: unknown) => { success: boolean } }; + }; + + expect(loaded.schema.safeParse("downstream").success).toBe(true); + expect(loaded.schema.safeParse("sideways").success).toBe(false); + }); it("does not redirect subpath imports such as @oh-my-pi/pi-ai/utils/schema", async () => { const entry = await writeFixtureExtension( @@ -124,6 +139,54 @@ describe("legacy pi package root remaps (issue #1474)", () => { expect(loaded.loadedVersion).toMatch(/^\d+\.\d+\.\d+/); }); + it("preserves legacy defineTool root imports for tool factory helpers", async () => { + const entry = await writeFixtureExtension( + [ + 'import { createCodingTools, defineTool, Type } from "@earendil-works/pi-coding-agent";', + "const definition = {", + '\tname: "legacy_define_tool",', + '\tlabel: "Legacy Define Tool",', + '\tdescription: "legacy helper probe",', + "\tparameters: Type.Object({}),", + '\texecute: async () => ({ content: [{ type: "text", text: "ok" }] }),', + "};", + "export const tool = defineTool(definition);", + "export const sameReference = tool === definition;", + "export const codingTools = createCodingTools(process.cwd());", + ].join("\n"), + ); + + const loaded = (await loadLegacyPiModule(entry)) as { + tool: { name: string; parameters: { safeParse: (input: unknown) => { success: boolean } } }; + sameReference: boolean; + codingTools: unknown[]; + }; + + expect(loaded.sameReference).toBe(true); + expect(loaded.tool.name).toBe("legacy_define_tool"); + expect(Array.isArray(loaded.codingTools)).toBe(true); + }); + + it("preserves legacy frontmatter helper root imports", async () => { + const entry = await writeFixtureExtension( + [ + 'import { parseFrontmatter, stripFrontmatter } from "@earendil-works/pi-coding-agent";', + "const content = ['---', 'name: demo', '---', '# Body'].join('\\n');", + "export const parsed = parseFrontmatter(content);", + "export const stripped = stripFrontmatter(content);", + ].join("\n"), + ); + + const loaded = (await loadLegacyPiModule(entry)) as { + parsed: { frontmatter: { name?: string }; body: string }; + stripped: string; + }; + + expect(loaded.parsed.frontmatter.name).toBe("demo"); + expect(loaded.parsed.body).toBe("# Body"); + expect(loaded.stripped).toBe("# Body"); + }); + it("falls back to legacy-scoped subpath peers for direct plugin imports", async () => { const realResolveSync = Bun.resolveSync.bind(Bun); vi.spyOn(Bun, "resolveSync").mockImplementation((specifier: string, from: string) => { diff --git a/packages/coding-agent/test/extensibility/typebox-remap.test.ts b/packages/coding-agent/test/extensibility/typebox-remap.test.ts index a1709a017..220993ae2 100644 --- a/packages/coding-agent/test/extensibility/typebox-remap.test.ts +++ b/packages/coding-agent/test/extensibility/typebox-remap.test.ts @@ -31,7 +31,7 @@ async function writeFixtureExtension(source: string): Promise { return entry; } -describe("legacy-pi @sinclair/typebox remap", () => { +describe("legacy-pi TypeBox remap", () => { it("redirects bare @sinclair/typebox imports inside legacy extensions to the in-repo shim", async () => { const entry = await writeFixtureExtension( [ @@ -50,4 +50,23 @@ describe("legacy-pi @sinclair/typebox remap", () => { expect(loaded.objectSchema.safeParse({ name: "ok" }).success).toBe(true); expect(loaded.objectSchema.safeParse({ name: "ok", extra: 1 }).success).toBe(false); }); + + it("redirects bare typebox imports inside legacy extensions to the in-repo shim", async () => { + const entry = await writeFixtureExtension( + [ + 'import { Type } from "typebox";', + "export const probe = Type;", + "export const enumSchema = Type.Enum(['upstream', 'downstream']);", + ].join("\n"), + ); + + const loaded = (await loadLegacyPiModule(entry)) as { + probe: typeof TypeBoxShimType; + enumSchema: { safeParse: (input: unknown) => { success: boolean } }; + }; + + expect(loaded.probe).toBe(TypeBoxShimType); + expect(loaded.enumSchema.safeParse("upstream").success).toBe(true); + expect(loaded.enumSchema.safeParse("sideways").success).toBe(false); + }); }); From d2d876b129b6d027a17f509155cdf64fbbcb37fd Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 18 Jun 2026 02:10:39 +0000 Subject: [PATCH 2/3] fix(cli): preserved legacy shim behavior Restored StringEnum's plain string enum wire schema and returned legacy coding tool placeholders that let createAgentSession keep the real built-in tools active. Fixes #2858 --- .../src/extensibility/legacy-pi-ai-shim.ts | 30 +++++++++++++- .../legacy-pi-coding-agent-shim.ts | 39 ++++++++++++++++++- packages/coding-agent/src/sdk.ts | 11 ++++-- .../legacy-pi-ai-type-remap.test.ts | 16 ++++++-- 4 files changed, 86 insertions(+), 10 deletions(-) diff --git a/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts b/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts index 18c0ac1e7..cbe230d1b 100644 --- a/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts +++ b/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts @@ -19,6 +19,7 @@ * `types.ts` via the `export *` below — pi-ai still exports both as types, * only the runtime `Type` builder and `StringEnum()` helper were removed. */ +import { z } from "zod/v4"; import { type TSchema, Type } from "./typebox"; export interface StringEnumOptions { @@ -28,8 +29,35 @@ export interface StringEnumOptions { [key: string]: unknown; } +function stringEnumWireSchema(values: readonly T[], options: StringEnumOptions | undefined) { + const schema: Record = { + type: "string", + enum: [...values], + }; + if (!options) return schema; + for (const key in options) { + if (options[key] !== undefined) { + schema[key] = options[key]; + } + } + return schema; +} + export function StringEnum(values: readonly T[], options?: StringEnumOptions): TSchema { - return Type.Enum(values, options); + let schema: TSchema = + values.length === 0 + ? z.never() + : z.enum(values).describe(options?.description ?? "Legacy string enum compatibility schema"); + if (options && "default" in options) { + schema = schema.default(options.default); + } + Object.defineProperty(schema, "toJSON", { + value: () => stringEnumWireSchema(values, options), + enumerable: false, + writable: true, + configurable: true, + }); + return schema; } export * from "@oh-my-pi/pi-ai"; diff --git a/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts b/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts index 1cb8104d4..fe0015f4f 100644 --- a/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts +++ b/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts @@ -18,6 +18,41 @@ import { Settings } from "../config/settings"; import type { ToolDefinition } from "./extensions/types"; import { Type } from "./typebox"; +const TOOL_DEFINITION_MARKER = "__isToolDefinition"; +const LEGACY_BUILTIN_TOOL_MARKER = "__ompLegacyBuiltinTool"; +const LEGACY_CODING_TOOL_NAMES = ["read", "bash", "edit", "write"] as const; + +type LegacyCodingToolName = (typeof LEGACY_CODING_TOOL_NAMES)[number]; +type LegacyBuiltinToolDefinition = ToolDefinition & { [LEGACY_BUILTIN_TOOL_MARKER]: true }; + +function markToolDefinition( + tool: ToolDefinition, +): ToolDefinition { + Object.defineProperty(tool, TOOL_DEFINITION_MARKER, { + value: true, + enumerable: false, + writable: false, + configurable: true, + }); + return tool; +} + +function legacyBuiltinTool(name: LegacyCodingToolName): ToolDefinition { + const tool: LegacyBuiltinToolDefinition = { + name, + label: name, + description: `Built-in ${name} tool placeholder resolved by createAgentSession.`, + parameters: Type.Object({}), + execute: async () => { + throw new Error( + `Legacy createCodingTools() returned ${name}; pass it through createAgentSession({ customTools }) so the SDK can bind the built-in implementation.`, + ); + }, + [LEGACY_BUILTIN_TOOL_MARKER]: true, + }; + return markToolDefinition(tool); +} + export interface ParsedFrontmatter = Record> { frontmatter: T; body: string; @@ -37,11 +72,11 @@ export function stripFrontmatter(content: string): string { export function defineTool( tool: ToolDefinition, ): ToolDefinition { - return tool; + return markToolDefinition(tool); } export function createCodingTools(_cwd: string): ToolDefinition[] { - return []; + return LEGACY_CODING_TOOL_NAMES.map(legacyBuiltinTool); } export const SettingsManager = { diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 1e6b07235..9099f4979 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -872,6 +872,10 @@ function isCustomTool(tool: CustomTool | ToolDefinition): tool is CustomTool { return !(tool as any).__isToolDefinition; } +function isLegacyBuiltinToolDefinition(tool: CustomTool | ToolDefinition): boolean { + return !isCustomTool(tool) && "__ompLegacyBuiltinTool" in tool && tool.__ompLegacyBuiltinTool === true; +} + const TOOL_DEFINITION_MARKER = Symbol("__isToolDefinition"); /** Matches the truncation applied to per-server instructions inside `rebuildSystemPrompt`. */ @@ -2008,12 +2012,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} const toolContextStore = new ToolContextStore(getSessionContext); const registeredTools = extensionRunner.getAllRegisteredTools(); + const sdkCustomTools = options.customTools?.filter(tool => !isLegacyBuiltinToolDefinition(tool)) ?? []; const allCustomTools = [ ...registeredTools, - ...(options.customTools?.map(tool => { + ...sdkCustomTools.map(tool => { const definition = isCustomTool(tool) ? customToolToDefinition(tool) : tool; return { definition, extensionPath: "" }; - }) ?? []), + }), ]; // `wrapToolWithMetaNotice` runs the centralized large-output → artifact spill. // Built-in tools get it in `createTools`; extension, SDK-custom, image-gen, @@ -2291,7 +2296,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} // Custom tools and extension-registered tools are always included regardless of toolNames filter const alwaysInclude: string[] = [ - ...(options.customTools?.map(t => (isCustomTool(t) ? t.name : t.name)) ?? []), + ...sdkCustomTools.map(t => (isCustomTool(t) ? t.name : t.name)), ...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name), ]; for (const name of alwaysInclude) { diff --git a/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts b/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts index 6f0ebf583..64edabaea 100644 --- a/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts +++ b/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts @@ -88,20 +88,28 @@ describe("legacy-pi @(scope)/pi-ai root `Type` remap (issue #1437)", () => { expect(loaded.zodObj.safeParse({ name: "ok" }).success).toBe(true); expect(loaded.zodObj.safeParse({}).success).toBe(false); }); - it("preserves legacy StringEnum root imports as TypeBox-compatible schemas", async () => { + it("preserves legacy StringEnum root imports as plain string enum schemas", async () => { const entry = await writeFixtureExtension( [ 'import { StringEnum } from "@earendil-works/pi-ai";', 'export const schema = StringEnum(["upstream", "downstream"] as const, { default: "upstream" });', + "export const wireSchema = JSON.parse(JSON.stringify(schema));", ].join("\n"), ); const loaded = (await loadLegacyPiModule(entry)) as { schema: { safeParse: (input: unknown) => { success: boolean } }; + wireSchema: { type?: string; enum?: string[]; default?: string; anyOf?: unknown }; }; expect(loaded.schema.safeParse("downstream").success).toBe(true); expect(loaded.schema.safeParse("sideways").success).toBe(false); + expect(loaded.wireSchema).toEqual({ + type: "string", + enum: ["upstream", "downstream"], + default: "upstream", + }); + expect(loaded.wireSchema.anyOf).toBeUndefined(); }); it("does not redirect subpath imports such as @oh-my-pi/pi-ai/utils/schema", async () => { @@ -152,19 +160,19 @@ describe("legacy pi package root remaps (issue #1474)", () => { "};", "export const tool = defineTool(definition);", "export const sameReference = tool === definition;", - "export const codingTools = createCodingTools(process.cwd());", + "export const codingToolNames = createCodingTools(process.cwd()).map(tool => tool.name);", ].join("\n"), ); const loaded = (await loadLegacyPiModule(entry)) as { tool: { name: string; parameters: { safeParse: (input: unknown) => { success: boolean } } }; sameReference: boolean; - codingTools: unknown[]; + codingToolNames: string[]; }; expect(loaded.sameReference).toBe(true); expect(loaded.tool.name).toBe("legacy_define_tool"); - expect(Array.isArray(loaded.codingTools)).toBe(true); + expect(loaded.codingToolNames).toEqual(["read", "bash", "edit", "write"]); }); it("preserves legacy frontmatter helper root imports", async () => { From 71481ab47e38903e377bf5b4f2e228d32a555821 Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 18 Jun 2026 02:37:53 +0000 Subject: [PATCH 3/3] fix(cli): returned usable legacy coding tools Changed createCodingTools compatibility exports to instantiate cwd-bound built-in read/bash/edit/write tool definitions instead of throwing placeholders. Fixes #2858 --- .../legacy-pi-coding-agent-shim.ts | 63 ++++++++++++++----- .../legacy-pi-ai-type-remap.test.ts | 20 +++++- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts b/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts index fe0015f4f..28731ca40 100644 --- a/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts +++ b/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts @@ -12,9 +12,11 @@ * the same module identity as a direct `@oh-my-pi/pi-coding-agent` import. */ +import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core"; import type { TSchema } from "@oh-my-pi/pi-ai"; import { parseFrontmatter as parseOmpFrontmatter } from "@oh-my-pi/pi-utils"; import { Settings } from "../config/settings"; +import { BUILTIN_TOOLS, type Tool, type ToolSession } from "../tools"; import type { ToolDefinition } from "./extensions/types"; import { Type } from "./typebox"; @@ -36,21 +38,54 @@ function markToolDefinition( }); return tool; } +function legacyToolSession(cwd: string): ToolSession { + return { + cwd, + hasUI: false, + getSessionFile: () => null, + getSessionSpawns: () => null, + settings: Settings.isolated(), + }; +} -function legacyBuiltinTool(name: LegacyCodingToolName): ToolDefinition { - const tool: LegacyBuiltinToolDefinition = { - name, - label: name, - description: `Built-in ${name} tool placeholder resolved by createAgentSession.`, - parameters: Type.Object({}), - execute: async () => { - throw new Error( - `Legacy createCodingTools() returned ${name}; pass it through createAgentSession({ customTools }) so the SDK can bind the built-in implementation.`, - ); - }, +function createBuiltinTool(cwd: string, name: LegacyCodingToolName): Tool { + const tool = BUILTIN_TOOLS[name](legacyToolSession(cwd)); + if (tool instanceof Promise) { + throw new Error(`Built-in ${name} tool factory unexpectedly returned a promise.`); + } + if (!tool) { + throw new Error(`Built-in ${name} tool is unavailable.`); + } + return tool; +} + +async function executeBuiltinTool( + cwd: string, + name: LegacyCodingToolName, + toolCallId: string, + params: unknown, + signal: AbortSignal | undefined, + onUpdate: AgentToolUpdateCallback | undefined, +) { + const tool = createBuiltinTool(cwd, name); + return tool.execute(toolCallId, params, signal, onUpdate); +} + +function legacyBuiltinTool(cwd: string, name: LegacyCodingToolName): ToolDefinition { + const tool = createBuiltinTool(cwd, name); + const definition: LegacyBuiltinToolDefinition = { + name: tool.name, + label: tool.label, + description: tool.description, + parameters: tool.parameters, + hidden: tool.hidden, + deferrable: tool.deferrable, + approval: tool.approval, + execute: (toolCallId, params, signal, onUpdate) => + executeBuiltinTool(cwd, name, toolCallId, params, signal, onUpdate), [LEGACY_BUILTIN_TOOL_MARKER]: true, }; - return markToolDefinition(tool); + return markToolDefinition(definition); } export interface ParsedFrontmatter = Record> { @@ -75,8 +110,8 @@ export function defineTool legacyBuiltinTool(cwd, name)); } export const SettingsManager = { diff --git a/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts b/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts index 64edabaea..e1dc937f5 100644 --- a/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts +++ b/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts @@ -147,9 +147,16 @@ describe("legacy pi package root remaps (issue #1474)", () => { expect(loaded.loadedVersion).toMatch(/^\d+\.\d+\.\d+/); }); - it("preserves legacy defineTool root imports for tool factory helpers", async () => { - const entry = await writeFixtureExtension( + it("preserves legacy defineTool root imports and usable coding tools", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-legacy-coding-tools-")); + tempRoots.push(dir); + await fs.writeFile(path.join(dir, "sample.txt"), "legacy read body", "utf8"); + const entry = path.join(dir, "index.ts"); + await fs.writeFile( + entry, [ + 'import { dirname } from "node:path";', + 'import { fileURLToPath } from "node:url";', 'import { createCodingTools, defineTool, Type } from "@earendil-works/pi-coding-agent";', "const definition = {", '\tname: "legacy_define_tool",', @@ -158,21 +165,28 @@ describe("legacy pi package root remaps (issue #1474)", () => { "\tparameters: Type.Object({}),", '\texecute: async () => ({ content: [{ type: "text", text: "ok" }] }),', "};", + "const cwd = dirname(fileURLToPath(import.meta.url));", + "const codingTools = createCodingTools(cwd);", + "const readTool = codingTools.find(tool => tool.name === 'read');", "export const tool = defineTool(definition);", "export const sameReference = tool === definition;", - "export const codingToolNames = createCodingTools(process.cwd()).map(tool => tool.name);", + "export const codingToolNames = codingTools.map(tool => tool.name);", + "export const readResult = await readTool?.execute('legacy-read', { path: 'sample.txt' });", ].join("\n"), + "utf8", ); const loaded = (await loadLegacyPiModule(entry)) as { tool: { name: string; parameters: { safeParse: (input: unknown) => { success: boolean } } }; sameReference: boolean; codingToolNames: string[]; + readResult: { content: Array<{ type: string; text?: string }> }; }; expect(loaded.sameReference).toBe(true); expect(loaded.tool.name).toBe("legacy_define_tool"); expect(loaded.codingToolNames).toEqual(["read", "bash", "edit", "write"]); + expect(loaded.readResult.content[0]?.text).toContain("legacy read body"); }); it("preserves legacy frontmatter helper root imports", async () => {