Merge remote-tracking branch 'origin/farm/39009a49/fix-extension-validation-shims'
This commit is contained in:
@@ -11,14 +11,54 @@
|
||||
* 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 { z } from "zod/v4";
|
||||
import { type TSchema, Type } from "./typebox";
|
||||
|
||||
export interface StringEnumOptions<T extends string> {
|
||||
description?: string;
|
||||
default?: T;
|
||||
examples?: T[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function stringEnumWireSchema<T extends string>(values: readonly T[], options: StringEnumOptions<T> | undefined) {
|
||||
const schema: Record<string, unknown> = {
|
||||
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<T extends string>(values: readonly T[], options?: StringEnumOptions<T>): TSchema {
|
||||
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";
|
||||
export { Type } from "./typebox";
|
||||
export { Type };
|
||||
|
||||
@@ -12,4 +12,117 @@
|
||||
* 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";
|
||||
|
||||
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<TParams extends TSchema, TDetails>(
|
||||
tool: ToolDefinition<TParams, TDetails>,
|
||||
): ToolDefinition<TParams, TDetails> {
|
||||
Object.defineProperty(tool, TOOL_DEFINITION_MARKER, {
|
||||
value: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
configurable: true,
|
||||
});
|
||||
return tool;
|
||||
}
|
||||
function legacyToolSession(cwd: string): ToolSession {
|
||||
return {
|
||||
cwd,
|
||||
hasUI: false,
|
||||
getSessionFile: () => null,
|
||||
getSessionSpawns: () => null,
|
||||
settings: Settings.isolated(),
|
||||
};
|
||||
}
|
||||
|
||||
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(definition);
|
||||
}
|
||||
|
||||
export interface ParsedFrontmatter<T extends Record<string, unknown> = Record<string, unknown>> {
|
||||
frontmatter: T;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export function parseFrontmatter<T extends Record<string, unknown> = Record<string, unknown>>(
|
||||
content: string,
|
||||
): ParsedFrontmatter<T> {
|
||||
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<TParams extends TSchema = TSchema, TDetails = unknown>(
|
||||
tool: ToolDefinition<TParams, TDetails>,
|
||||
): ToolDefinition<TParams, TDetails> {
|
||||
return markToolDefinition(tool);
|
||||
}
|
||||
|
||||
export function createCodingTools(cwd: string): ToolDefinition[] {
|
||||
return LEGACY_CODING_TOOL_NAMES.map(name => legacyBuiltinTool(cwd, name));
|
||||
}
|
||||
|
||||
export const SettingsManager = {
|
||||
create(cwd: string, agentDir?: string): Promise<Settings> {
|
||||
return Settings.init({ cwd, agentDir });
|
||||
},
|
||||
|
||||
inMemory(): Settings {
|
||||
return Settings.isolated();
|
||||
},
|
||||
} as const;
|
||||
|
||||
export * from "../index";
|
||||
export { Type };
|
||||
|
||||
@@ -64,14 +64,14 @@ const packageRootCache = new Map<string, string | null>();
|
||||
const packageImportsCache = new Map<string, Record<string, unknown> | 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.
|
||||
|
||||
@@ -528,6 +528,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<Settings>;
|
||||
|
||||
/** Whether UI is available (enables interactive tools like ask). Default: false */
|
||||
hasUI?: boolean;
|
||||
@@ -874,6 +879,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`. */
|
||||
@@ -1138,7 +1147,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();
|
||||
@@ -2008,12 +2019,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: "<sdk>" };
|
||||
}) ?? []),
|
||||
}),
|
||||
];
|
||||
// `wrapToolWithMetaNotice` runs the centralized large-output → artifact spill.
|
||||
// Built-in tools get it in `createTools`; extension, SDK-custom, image-gen,
|
||||
@@ -2291,7 +2303,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) {
|
||||
|
||||
Reference in New Issue
Block a user