From 83496b82117a5a6c749fcba30e4a39a732bda1b0 Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 5 Aug 2026 15:42:06 +0000 Subject: [PATCH] fix(coding-agent): returned ToolInfo[] from getAllTools extension API The ExtensionAPI getAllTools() wired to session.getAllToolNames(), returning bare tool-name strings. Upstream @earendil-works/pi-coding-agent promises ToolInfo[] with sourceInfo, so extensions loaded through the legacy-pi shim (e.g. gentle-pi) crashed on t.sourceInfo.source at every session start. Added SourceInfo/ToolInfo types plus SessionTools.getAllToolInfos(), which returns { name, description, parameters, sourceInfo } and classifies each tool as builtin/mcp/sdk/extension. Rewired every getAllTools action site (interactive, acp, print/rpc, subagent executor) and the example extension. Fixes #7732 --- packages/coding-agent/CHANGELOG.md | 4 + .../coding-agent/examples/extensions/tools.ts | 4 +- .../src/extensibility/extensions/loader.ts | 5 +- .../src/extensibility/extensions/types.ts | 36 +++++++- .../coding-agent/src/modes/acp/acp-agent.ts | 2 +- .../controllers/extension-ui-controller.ts | 4 +- .../coding-agent/src/modes/runtime-init.ts | 2 +- .../coding-agent/src/session/agent-session.ts | 6 ++ .../coding-agent/src/session/session-tools.ts | 29 ++++++- packages/coding-agent/src/task/executor.ts | 2 +- .../test/getalltools-toolinfo.test.ts | 87 +++++++++++++++++++ 11 files changed, 168 insertions(+), 13 deletions(-) create mode 100644 packages/coding-agent/test/getalltools-toolinfo.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 456e88609..86225b61b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `pi.getAllTools()` returning bare tool-name strings instead of `ToolInfo[]`, which crashed extensions authored against the upstream `@earendil-works/pi-coding-agent` contract (e.g. gentle-pi's startup banner: `undefined is not an object (evaluating 't.sourceInfo.source')`). The ExtensionAPI now returns `{ name, description, parameters, promptGuidelines, sourceInfo }` objects with `sourceInfo.source` classifying each tool as `builtin`/`sdk`/`mcp`/`extension` ([#7732](https://github.com/can1357/oh-my-pi/issues/7732)). + ## [17.2.9] - 2026-08-05 ### Breaking Changes diff --git a/packages/coding-agent/examples/extensions/tools.ts b/packages/coding-agent/examples/extensions/tools.ts index 178e1fc5e..fca16874a 100644 --- a/packages/coding-agent/examples/extensions/tools.ts +++ b/packages/coding-agent/examples/extensions/tools.ts @@ -36,7 +36,7 @@ export default function toolsExtension(pi: ExtensionAPI) { // Find the last tools-config entry in the current branch async function restoreFromBranch(ctx: ExtensionContext) { - allTools = pi.getAllTools(); + allTools = pi.getAllTools().map(t => t.name); // Get entries in current branch only const branchEntries = ctx.sessionManager.getBranch(); @@ -66,7 +66,7 @@ export default function toolsExtension(pi: ExtensionAPI) { description: "Enable/disable tools", handler: async (_args, ctx) => { // Refresh tool list - allTools = pi.getAllTools(); + allTools = pi.getAllTools().map(t => t.name); await ctx.ui.custom((tui, theme, _keybindings, done) => { // Build settings items for each tool diff --git a/packages/coding-agent/src/extensibility/extensions/loader.ts b/packages/coding-agent/src/extensibility/extensions/loader.ts index 9e8abae74..3763393d6 100644 --- a/packages/coding-agent/src/extensibility/extensions/loader.ts +++ b/packages/coding-agent/src/extensibility/extensions/loader.ts @@ -46,6 +46,7 @@ import type { ProviderConfig, RegisteredCommand, ToolDefinition, + ToolInfo, } from "./types"; installLegacyPiSpecifierShim(); @@ -92,7 +93,7 @@ export class ExtensionRuntime implements IExtensionRuntime { throw new ExtensionRuntimeNotInitializedError(); } - getAllTools(): string[] { + getAllTools(): ToolInfo[] { throw new ExtensionRuntimeNotInitializedError(); } @@ -245,7 +246,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime { return this.runtime.getActiveTools(); } - getAllTools(): string[] { + getAllTools(): ToolInfo[] { return this.runtime.getAllTools(); } diff --git a/packages/coding-agent/src/extensibility/extensions/types.ts b/packages/coding-agent/src/extensibility/extensions/types.ts index 49067f17b..000c8df40 100644 --- a/packages/coding-agent/src/extensibility/extensions/types.ts +++ b/packages/coding-agent/src/extensibility/extensions/types.ts @@ -596,6 +596,36 @@ export interface ToolDefinition Component; } +/** Whether a tool's source is scoped to the user, the project, or a transient runtime session. */ +export type SourceScope = "user" | "project" | "temporary"; + +/** Whether a tool's source came from an installed package or a top-level (loose) file. */ +export type SourceOrigin = "package" | "top-level"; + +/** + * Provenance metadata describing where a registered tool came from. Mirrors the + * `@earendil-works/pi-coding-agent` `SourceInfo` contract so extensions authored + * against upstream pi (e.g. gentle-pi) can read `sourceInfo.source` unchanged. + */ +export interface SourceInfo { + /** Synthetic or on-disk identifier for the tool's origin (e.g. ``). */ + path: string; + /** Origin class: `"builtin"`, `"sdk"`, `"mcp"`, or `"extension"`. */ + source: string; + scope: SourceScope; + origin: SourceOrigin; + baseDir?: string; +} + +/** Tool metadata returned by {@link ExtensionAPI.getAllTools}: identity, schema, and source provenance. */ +export interface ToolInfo { + name: string; + description: string; + parameters: TSchema; + promptGuidelines?: string[]; + sourceInfo: SourceInfo; +} + // ============================================================================ // Resource Events // ============================================================================ @@ -1267,8 +1297,8 @@ export interface ExtensionAPI { /** Get the list of currently active tool names. */ getActiveTools(): string[]; - /** Get all configured tools (built-in + extension tools). */ - getAllTools(): string[]; + /** Get all configured tools (built-in + extension tools) with schema and source metadata. */ + getAllTools(): ToolInfo[]; /** Set the active tools by name. */ setActiveTools(toolNames: string[]): Promise; @@ -1464,7 +1494,7 @@ export type AppendEntryHandler = (customType: string, data?: T) => export type GetActiveToolsHandler = () => string[]; -export type GetAllToolsHandler = () => string[]; +export type GetAllToolsHandler = () => ToolInfo[]; export type GetCommandsHandler = () => SlashCommandInfo[]; diff --git a/packages/coding-agent/src/modes/acp/acp-agent.ts b/packages/coding-agent/src/modes/acp/acp-agent.ts index d3d480564..26c81214c 100644 --- a/packages/coding-agent/src/modes/acp/acp-agent.ts +++ b/packages/coding-agent/src/modes/acp/acp-agent.ts @@ -2348,7 +2348,7 @@ export class AcpAgent implements Agent { record.session.sessionManager.appendLabelChange(targetId, label); }, getActiveTools: () => record.session.getEnabledToolNames(), - getAllTools: () => record.session.getAllToolNames(), + getAllTools: () => record.session.getAllToolInfos(), setActiveTools: toolNames => record.session.setActiveToolsByName(toolNames), getCommands: () => getSessionSlashCommands(record.session), setModel: async model => { diff --git a/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts b/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts index 942b03da8..4b2b57bf5 100644 --- a/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts +++ b/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts @@ -166,7 +166,7 @@ export class ExtensionUiController { this.ctx.sessionManager.appendLabelChange(targetId, label); }, getActiveTools: () => this.ctx.session.getEnabledToolNames(), - getAllTools: () => this.ctx.session.getAllToolNames(), + getAllTools: () => this.ctx.session.getAllToolInfos(), setActiveTools: toolNames => this.ctx.session.setActiveToolsByName(toolNames), setModel: async model => { const key = await this.ctx.session.modelRegistry.getApiKey(model); @@ -399,7 +399,7 @@ export class ExtensionUiController { this.ctx.sessionManager.appendLabelChange(targetId, label); }, getActiveTools: () => this.ctx.session.getEnabledToolNames(), - getAllTools: () => this.ctx.session.getAllToolNames(), + getAllTools: () => this.ctx.session.getAllToolInfos(), setActiveTools: toolNames => this.ctx.session.setActiveToolsByName(toolNames), setModel: async model => { const key = await this.ctx.session.modelRegistry.getApiKey(model); diff --git a/packages/coding-agent/src/modes/runtime-init.ts b/packages/coding-agent/src/modes/runtime-init.ts index 2ac16d977..54ffe8635 100644 --- a/packages/coding-agent/src/modes/runtime-init.ts +++ b/packages/coding-agent/src/modes/runtime-init.ts @@ -84,7 +84,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi session.sessionManager.appendLabelChange(targetId, label); }, getActiveTools: () => session.getEnabledToolNames(), - getAllTools: () => session.getAllToolNames(), + getAllTools: () => session.getAllToolInfos(), setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames), getCommands: () => getSessionSlashCommands(session), setModel: model => runExtensionSetModel(session, model), diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 9a34d5da4..73f67310a 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -127,6 +127,7 @@ import type { ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, + ToolInfo, TreePreparation, TurnEndEvent, TurnStartEvent, @@ -4181,6 +4182,11 @@ export class AgentSession { return this.#tools.getAllToolNames(); } + /** Full metadata for every registered tool, including source provenance (backs `getAllTools()`). */ + getAllToolInfos(): ToolInfo[] { + return this.#tools.getAllToolInfos(); + } + /** Installs and activates the ephemeral vibe tool set. */ activateVibeTools(baseToolNames: string[]): Promise { return this.#tools.activateVibeTools(baseToolNames); diff --git a/packages/coding-agent/src/session/session-tools.ts b/packages/coding-agent/src/session/session-tools.ts index e78ee3650..2585b760b 100644 --- a/packages/coding-agent/src/session/session-tools.ts +++ b/packages/coding-agent/src/session/session-tools.ts @@ -7,7 +7,7 @@ import { formatModelString } from "../config/model-resolver"; import type { Settings, SkillsSettings } from "../config/settings"; import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types"; import { CustomToolAdapter } from "../extensibility/custom-tools/wrapper"; -import type { ExtensionRunner } from "../extensibility/extensions"; +import type { ExtensionRunner, SourceInfo, ToolInfo } from "../extensibility/extensions"; import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper"; import { loadSkills, type Skill, type SkillWarning, setActiveSkills } from "../extensibility/skills"; import { type LocalProtocolOptions, XD_URL_PREFIX } from "../internal-urls"; @@ -333,6 +333,33 @@ export class SessionTools { return Array.from(this.#toolRegistry.keys()); } + /** + * Full metadata for every registered tool, including source provenance. + * + * Backs the `getAllTools()` ExtensionAPI method. Returns {@link ToolInfo} + * objects (not bare names) so extensions authored against upstream + * `@earendil-works/pi-coding-agent` — which promises `ToolInfo[]` — can read + * `sourceInfo.source` unchanged. + */ + getAllToolInfos(): ToolInfo[] { + return Array.from(this.#toolRegistry, ([name, tool]) => { + const source = this.#builtInToolNames.has(name) + ? "builtin" + : isMCPToolName(name) + ? "mcp" + : this.#rpcHostToolNames.has(name) + ? "sdk" + : "extension"; + const sourceInfo: SourceInfo = { + path: `<${source}:${name}>`, + source, + scope: "temporary", + origin: "top-level", + }; + return { name, description: tool.description, parameters: tool.parameters, sourceInfo }; + }); + } + #wrapRuntimeTool(tool: AgentTool): AgentTool { const wrapped = wrapToolWithMetaNotice(tool); const extensionRunner = this.#host.extensionRunner(); diff --git a/packages/coding-agent/src/task/executor.ts b/packages/coding-agent/src/task/executor.ts index c4db555f3..fd2f9737a 100644 --- a/packages/coding-agent/src/task/executor.ts +++ b/packages/coding-agent/src/task/executor.ts @@ -3179,7 +3179,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise session.getEnabledToolNames(), - getAllTools: () => session.getAllToolNames(), + getAllTools: () => session.getAllToolInfos(), setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames.filter(name => !isParentOwnedTool(name))), getCommands: () => getSessionSlashCommands(session), diff --git a/packages/coding-agent/test/getalltools-toolinfo.test.ts b/packages/coding-agent/test/getalltools-toolinfo.test.ts new file mode 100644 index 000000000..ed3d04968 --- /dev/null +++ b/packages/coding-agent/test/getalltools-toolinfo.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "bun:test"; +import * as path from "node:path"; +import { type } from "@oh-my-pi/omptype"; +import { Agent, type AgentTool } from "@oh-my-pi/pi-agent-core"; +import { createMockModel } from "@oh-my-pi/pi-ai/providers/mock"; +import { buildModel } from "@oh-my-pi/pi-catalog/build"; +import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry"; +import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; +import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session"; +import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage"; +import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; +import { TempDir } from "@oh-my-pi/pi-utils"; + +function createTool(name: string): AgentTool { + return { + name, + label: name, + description: `${name} tool`, + parameters: type({}), + async execute() { + return { content: [{ type: "text", text: name }] }; + }, + }; +} + +describe("AgentSession.getAllToolInfos", () => { + it("returns ToolInfo objects with sourceInfo so upstream-pi extensions read sourceInfo.source", async () => { + const tempDir = TempDir.createSync("@getalltools-toolinfo-"); + const authStorage = await AuthStorage.create(path.join(tempDir.path(), "auth.db")); + authStorage.setRuntimeApiKey("anthropic", "test-key"); + const settings = Settings.isolated({ "compaction.enabled": false }); + const model = buildModel({ + id: "mock", + name: "mock", + api: "openai-responses", + provider: "openai", + baseUrl: "https://example.invalid", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 8192, + maxTokens: 2048, + }); + const read = createTool("read"); + const custom = createTool("my_ext_tool"); + const agent = new Agent({ + getApiKey: () => "test-key", + initialState: { model, systemPrompt: ["initial"], tools: [read] }, + streamFn: createMockModel({ responses: [{ content: ["ok"] }] }).stream, + }); + // `read` is a built-in; `my_ext_tool` is registered without being marked + // built-in, so it must classify as an extension-sourced tool. + const toolRegistry = new Map([ + [read.name, read], + [custom.name, custom], + ]); + const session = new AgentSession({ + agent, + sessionManager: SessionManager.inMemory(tempDir.path()), + settings, + modelRegistry: new ModelRegistry(authStorage, path.join(tempDir.path(), "models.yml")), + toolRegistry, + builtInToolNames: [read.name], + rebuildSystemPrompt: async toolNames => ({ systemPrompt: [toolNames.join(",")] }), + }); + + try { + const allTools = session.getAllToolInfos(); + const byName = new Map(allTools.map(t => [t.name, t])); + + expect(byName.get("read")?.sourceInfo.source).toBe("builtin"); + expect(byName.get("my_ext_tool")?.sourceInfo.source).toBe("extension"); + // ToolInfo carries schema + description, not just a name. + expect(byName.get("read")?.description).toBe("read tool"); + expect(byName.get("read")?.parameters).toBeDefined(); + + // gentle-pi's startup-banner.ts filter must not throw and must treat + // only non-builtin/non-sdk tools as "custom". + const customTools = allTools.filter(t => !["builtin", "sdk"].includes(t.sourceInfo.source)); + expect(customTools.map(t => t.name)).toEqual(["my_ext_tool"]); + } finally { + await session.dispose(); + authStorage.close(); + tempDir.removeSync(); + } + }); +});