Add customTools option back to createAgentSession SDK

- Accepts ToolDefinition[] directly (simplified from old { path?, tool } format)
- Tools are combined with extension-registered tools
- Updated sdk.md documentation
- Updated CHANGELOG
This commit is contained in:
Mario Zechner 2026-01-05 03:34:36 +01:00
parent 6bd5e419a6
commit 8da793b1ba
4 changed files with 56 additions and 101 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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: "<sdk>" })) ?? []),
];
const wrappedExtensionTools = wrapRegisteredTools(allCustomTools, () => ({
ui: extensionRunner?.getUIContext() ?? {
select: async () => undefined,
confirm: async () => false,