diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8f304397..25836f92 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -163,11 +163,14 @@ pi --extension ./safety.ts -e ./todo.ts **Runner and wrapper:** - `HookRunner` → `ExtensionRunner` - `wrapToolsWithHooks()` → `wrapToolsWithExtensions()` -- `wrapToolWithHook()` → `wrapToolWithExtensions()` +- `wrapToolWithHooks()` → `wrapToolWithExtensions()` **CreateAgentSessionOptions:** -- `.hooks` → `.extensions` -- `.customTools` → merged into `.extensions` +- `.hooks` → removed (use `.additionalExtensionPaths` for paths) +- `.additionalHookPaths` → `.additionalExtensionPaths` +- `.preloadedHooks` → `.preloadedExtensions` +- `.customTools` type changed: `Array<{ path?; tool: CustomTool }>` → `ToolDefinition[]` +- `.additionalCustomToolPaths` → merged into `.additionalExtensionPaths` - `.slashCommands` → `.promptTemplates` **AgentSession:** @@ -194,6 +197,8 @@ pi --extension ./safety.ts -e ./todo.ts - Documentation: `docs/hooks.md` and `docs/custom-tools.md` merged into `docs/extensions.md` - Examples: `examples/hooks/` and `examples/custom-tools/` merged into `examples/extensions/` - README: Extensions section expanded with custom tools, commands, events, state persistence, shortcuts, flags, and UI examples +- SDK: `customTools` option now accepts `ToolDefinition[]` directly (simplified from `Array<{ path?, tool }>`) +- SDK: `additionalExtensionPaths` replaces both `additionalHookPaths` and `additionalCustomToolPaths` ## [0.34.2] - 2026-01-04 diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 09a2219c..724e1896 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -811,8 +811,8 @@ pi.registerTool({ details: { progress: 50 }, }); - // Run commands with cancellation support - const result = await ctx.exec("some-command", [], { signal }); + // Run commands via pi.exec (captured from extension closure) + const result = await pi.exec("some-command", [], { signal }); // Return result return { @@ -941,9 +941,9 @@ ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error" ctx.ui.setStatus("my-ext", "Processing..."); ctx.ui.setStatus("my-ext", undefined); // Clear -// Widget above editor (string array or Component) +// Widget above editor (string array or factory function) ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]); -ctx.ui.setWidget("my-widget", new Text(theme.fg("accent", "Custom"), 0, 0)); +ctx.ui.setWidget("my-widget", (tui, theme) => new Text(theme.fg("accent", "Custom"), 0, 0)); ctx.ui.setWidget("my-widget", undefined); // Clear // Terminal title diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index a1d604f6..b946af55 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -440,125 +440,60 @@ const { session } = await createAgentSession({ ```typescript import { Type } from "@sinclair/typebox"; -import { createAgentSession, discoverCustomTools, type CustomTool } from "@mariozechner/pi-coding-agent"; +import { createAgentSession, type ToolDefinition } from "@mariozechner/pi-coding-agent"; // Inline custom tool -const myTool: CustomTool = { +const myTool: ToolDefinition = { name: "my_tool", label: "My Tool", description: "Does something useful", parameters: Type.Object({ input: Type.String({ description: "Input value" }), }), - execute: async (toolCallId, params) => ({ + execute: async (toolCallId, params, onUpdate, ctx, signal) => ({ content: [{ type: "text", text: `Result: ${params.input}` }], details: {}, }), }; -// Replace discovery with inline tools +// Pass custom tools directly const { session } = await createAgentSession({ - customTools: [{ tool: myTool }], -}); - -// Merge with discovered tools (share eventBus for tool.events communication) -import { createEventBus } from "@mariozechner/pi-coding-agent"; - -const eventBus = createEventBus(); -const discovered = await discoverCustomTools(eventBus); -const { session } = await createAgentSession({ - customTools: [...discovered, { tool: myTool }], - eventBus, -}); - -// Add paths without replacing discovery -const { session } = await createAgentSession({ - additionalCustomToolPaths: ["/extra/tools"], + customTools: [myTool], }); ``` +Custom tools passed via `customTools` are combined with extension-registered tools. Extensions discovered from `~/.pi/agent/extensions/` and `.pi/extensions/` can also register tools via `pi.registerTool()`. + > See [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts) -### Hooks +### Extensions + +Extensions are discovered from `~/.pi/agent/extensions/` and `.pi/extensions/`. Use `additionalExtensionPaths` to add extra paths: ```typescript -import { createAgentSession, discoverHooks, type HookFactory } from "@mariozechner/pi-coding-agent"; +import { createAgentSession } from "@mariozechner/pi-coding-agent"; -// Inline hook -const loggingHook: HookFactory = (api) => { - // Log tool calls - api.on("tool_call", async (event) => { - console.log(`Tool: ${event.toolName}`); - return undefined; // Don't block - }); - - // Block dangerous commands - api.on("tool_call", async (event) => { - if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) { - return { block: true, reason: "Dangerous command" }; - } - return undefined; - }); - - // Register custom prompt template - api.registerCommand("stats", { - description: "Show session stats", - handler: async (ctx) => { - const entries = ctx.sessionManager.getEntries(); - ctx.ui.notify(`${entries.length} entries`, "info"); - }, - }); - - // Inject messages - api.sendMessage({ - customType: "my-hook", - content: "Hook initialized", - display: false, // Hidden from TUI - }, false); // Don't trigger agent turn - - // Persist hook state - api.appendEntry("my-hook", { initialized: true }); -}; - -// Replace discovery +// Add extension paths (merged with discovery) const { session } = await createAgentSession({ - hooks: [{ factory: loggingHook }], -}); - -// Disable all hooks -const { session } = await createAgentSession({ - hooks: [], -}); - -// Merge with discovered (share eventBus for pi.events communication) -import { createEventBus } from "@mariozechner/pi-coding-agent"; - -const eventBus = createEventBus(); -const discovered = await discoverHooks(eventBus); -const { session } = await createAgentSession({ - hooks: [...discovered, { factory: loggingHook }], - eventBus, -}); - -// Add paths without replacing -const { session } = await createAgentSession({ - additionalHookPaths: ["/extra/hooks"], + additionalExtensionPaths: ["/path/to/my-extension.ts"], }); ``` -**Event Bus:** If hooks or tools use `pi.events` for inter-component communication, pass the same `eventBus` to `discoverHooks()`, `discoverCustomTools()`, and `createAgentSession()`. Otherwise each gets an isolated bus and events won't be shared. +Extensions can register tools, subscribe to events, add commands, and more. See [extensions.md](extensions.md) for the full API. -Hook API methods: -- `api.on(event, handler)` - Subscribe to lifecycle events -- `api.events.emit(channel, data)` - Emit to shared event bus -- `api.events.on(channel, handler)` - Listen on shared event bus -- `api.sendMessage(message, triggerTurn?)` - Inject message (creates `CustomMessageEntry`) -- `api.appendEntry(customType, data?)` - Persist hook state (not in LLM context) -- `api.registerCommand(name, options)` - Register custom command -- `api.registerMessageRenderer(customType, renderer)` - Custom TUI rendering -- `api.exec(command, args, options?)` - Execute shell commands +**Event Bus:** Extensions can communicate via `pi.events`. Pass a shared `eventBus` to `createAgentSession()` if you need to emit/listen from outside: -> See [examples/sdk/06-hooks.ts](../examples/sdk/06-hooks.ts) and [docs/hooks.md](hooks.md) +```typescript +import { createAgentSession, createEventBus } from "@mariozechner/pi-coding-agent"; + +const eventBus = createEventBus(); +const { session } = await createAgentSession({ eventBus }); + +// Listen for events from extensions +eventBus.on("my-extension:status", (data) => console.log(data)); +``` + +> See [examples/sdk/06-extensions.ts](../examples/sdk/06-extensions.ts) and [docs/extensions.md](extensions.md) ### Skills diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index ffd0f7ab..af5dbb55 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -32,6 +32,7 @@ import { ExtensionRunner, type LoadExtensionsResult, type LoadedExtension, + type ToolDefinition, wrapRegisteredTools, wrapToolsWithExtensions, } from "./extensions/index.js"; @@ -96,6 +97,8 @@ export interface CreateAgentSessionOptions { /** Built-in tools to use. Default: codingTools [read, bash, edit, write] */ tools?: Tool[]; + /** Custom tools to register (in addition to built-in tools). */ + customTools?: ToolDefinition[]; /** Additional extension paths to load (merged with discovery). */ additionalExtensionPaths?: string[]; /** Pre-loaded extensions (skips loading, used when extensions were loaded early for CLI flags). */ @@ -130,7 +133,13 @@ export interface CreateAgentSessionResult { // Re-exports -export type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, ExtensionFactory } from "./extensions/index.js"; +export type { + ExtensionAPI, + ExtensionCommandContext, + ExtensionContext, + ExtensionFactory, + ToolDefinition, +} from "./extensions/index.js"; export type { PromptTemplate } from "./prompt-templates.js"; export type { Settings, SkillsSettings } from "./settings-manager.js"; export type { Skill } from "./skills.js"; @@ -444,11 +453,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} extensionRunner = new ExtensionRunner(extensionsResult.extensions, cwd, sessionManager, modelRegistry); } - // Wrap extension-registered tools with context getter (agent/session assigned below, accessed at execute time) + // Wrap extension-registered tools and SDK-provided custom tools with context getter + // (agent/session assigned below, accessed at execute time) let agent: Agent; let session: AgentSession; const registeredTools = extensionRunner?.getAllRegisteredTools() ?? []; - const wrappedExtensionTools = wrapRegisteredTools(registeredTools, () => ({ + // Combine extension-registered tools with SDK-provided custom tools + const allCustomTools = [ + ...registeredTools, + ...(options.customTools?.map((def) => ({ definition: def, extensionPath: "" })) ?? []), + ]; + const wrappedExtensionTools = wrapRegisteredTools(allCustomTools, () => ({ ui: extensionRunner?.getUIContext() ?? { select: async () => undefined, confirm: async () => false,