From a8a0f4b9fb9b1b5f3fadb41ec94342b71acbe6ea Mon Sep 17 00:00:00 2001 From: G Date: Sun, 1 Feb 2026 15:19:24 +0100 Subject: [PATCH] feat(coding-agent): type ToolCallEvent.input per tool Matches ToolResultEvent pattern with typed inputs via discriminated union. - Export *ToolInput types from tool schemas - Add *ToolCallEvent interfaces for each built-in tool - Add isToolCallEventType() guard with overloads for built-ins Direct narrowing (event.toolName === "bash") doesn't work due to CustomToolCallEvent.toolName: string overlapping with literals. Co-Authored-By: Claude Opus 4.5 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/docs/extensions.md | 39 +++++++- .../coding-agent/src/core/extensions/index.ts | 10 ++ .../coding-agent/src/core/extensions/types.ts | 98 ++++++++++++++++++- packages/coding-agent/src/core/tools/bash.ts | 4 +- packages/coding-agent/src/core/tools/edit.ts | 4 +- packages/coding-agent/src/core/tools/find.ts | 4 +- packages/coding-agent/src/core/tools/grep.ts | 4 +- packages/coding-agent/src/core/tools/index.ts | 46 ++++++++- packages/coding-agent/src/core/tools/ls.ts | 4 +- packages/coding-agent/src/core/tools/read.ts | 4 +- packages/coding-agent/src/core/tools/write.ts | 4 +- packages/coding-agent/src/index.ts | 16 +++ 13 files changed, 219 insertions(+), 19 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 758c96af..60adebaf 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Added typed `ToolCallEvent.input` per tool with `isToolCallEventType()` type guard for narrowing built-in tool events ([#1147](https://github.com/badlogic/pi-mono/pull/1147) by [@giuseppeg](https://github.com/giuseppeg)) - Exported `discoverAndLoadExtensions` from package to enable extension testing without a local repo clone ([#1148](https://github.com/badlogic/pi-mono/issues/1148)) - Added Extension UI Protocol documentation to RPC docs covering all request/response types for extension dialogs and notifications ([#1144](https://github.com/badlogic/pi-mono/pull/1144) by [@aliou](https://github.com/aliou)) - Added `rpc-demo.ts` example extension exercising all RPC-supported extension UI methods ([#1144](https://github.com/badlogic/pi-mono/pull/1144) by [@aliou](https://github.com/aliou)) diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 5c450a50..089ec377 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -473,16 +473,49 @@ Use this to update UI elements (status bars, footers) or perform model-specific #### tool_call -Fired before tool executes. **Can block.** +Fired before tool executes. **Can block.** Use `isToolCallEventType` to narrow and get typed inputs. ```typescript +import { isToolCallEventType } from "@mariozechner/pi-coding-agent"; + pi.on("tool_call", async (event, ctx) => { // event.toolName - "bash", "read", "write", "edit", etc. // event.toolCallId // event.input - tool parameters - if (shouldBlock(event)) { - return { block: true, reason: "Not allowed" }; + // Built-in tools: no type params needed + if (isToolCallEventType("bash", event)) { + // event.input is { command: string; timeout?: number } + if (event.input.command.includes("rm -rf")) { + return { block: true, reason: "Dangerous command" }; + } + } + + if (isToolCallEventType("read", event)) { + // event.input is { path: string; offset?: number; limit?: number } + console.log(`Reading: ${event.input.path}`); + } +}); +``` + +#### Typing custom tool input + +Custom tools should export their input type: + +```typescript +// my-extension.ts +export type MyToolInput = Static; +``` + +Use `isToolCallEventType` with explicit type parameters: + +```typescript +import { isToolCallEventType } from "@mariozechner/pi-coding-agent"; +import type { MyToolInput } from "my-extension"; + +pi.on("tool_call", (event) => { + if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) { + event.input.action; // typed } }); ``` diff --git a/packages/coding-agent/src/core/extensions/index.ts b/packages/coding-agent/src/core/extensions/index.ts index eb788072..7a3e3ad0 100644 --- a/packages/coding-agent/src/core/extensions/index.ts +++ b/packages/coding-agent/src/core/extensions/index.ts @@ -25,6 +25,8 @@ export type { // App keybindings (for custom editors) AppAction, AppendEntryHandler, + // Events - Tool (ToolCallEvent types) + BashToolCallEvent, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, @@ -35,7 +37,9 @@ export type { // Event Results ContextEventResult, ContextUsage, + CustomToolCallEvent, CustomToolResultEvent, + EditToolCallEvent, EditToolResultEvent, ExecOptions, ExecResult, @@ -59,10 +63,12 @@ export type { ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, + FindToolCallEvent, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetThinkingLevelHandler, + GrepToolCallEvent, GrepToolResultEvent, // Events - Input InputEvent, @@ -70,6 +76,7 @@ export type { InputSource, KeybindingsManager, LoadExtensionsResult, + LsToolCallEvent, LsToolResultEvent, // Message Rendering MessageRenderer, @@ -79,6 +86,7 @@ export type { // Provider Registration ProviderConfig, ProviderModelConfig, + ReadToolCallEvent, ReadToolResultEvent, // Commands RegisteredCommand, @@ -124,6 +132,7 @@ export type { UserBashEvent, UserBashEventResult, WidgetPlacement, + WriteToolCallEvent, WriteToolResultEvent, } from "./types.js"; // Type guards @@ -134,6 +143,7 @@ export { isGrepToolResult, isLsToolResult, isReadToolResult, + isToolCallEventType, isWriteToolResult, } from "./types.js"; export { diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 0ef7f614..efdb6027 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -57,10 +57,17 @@ import type { BashOperations } from "../tools/bash.js"; import type { EditToolDetails } from "../tools/edit.js"; import type { BashToolDetails, + BashToolInput, + EditToolInput, FindToolDetails, + FindToolInput, GrepToolDetails, + GrepToolInput, LsToolDetails, + LsToolInput, ReadToolDetails, + ReadToolInput, + WriteToolInput, } from "../tools/index.js"; export type { ExecOptions, ExecResult } from "../exec.js"; @@ -547,14 +554,62 @@ export type InputEventResult = // Tool Events // ============================================================================ -/** Fired before a tool executes. Can block. */ -export interface ToolCallEvent { +interface ToolCallEventBase { type: "tool_call"; - toolName: string; toolCallId: string; +} + +export interface BashToolCallEvent extends ToolCallEventBase { + toolName: "bash"; + input: BashToolInput; +} + +export interface ReadToolCallEvent extends ToolCallEventBase { + toolName: "read"; + input: ReadToolInput; +} + +export interface EditToolCallEvent extends ToolCallEventBase { + toolName: "edit"; + input: EditToolInput; +} + +export interface WriteToolCallEvent extends ToolCallEventBase { + toolName: "write"; + input: WriteToolInput; +} + +export interface GrepToolCallEvent extends ToolCallEventBase { + toolName: "grep"; + input: GrepToolInput; +} + +export interface FindToolCallEvent extends ToolCallEventBase { + toolName: "find"; + input: FindToolInput; +} + +export interface LsToolCallEvent extends ToolCallEventBase { + toolName: "ls"; + input: LsToolInput; +} + +export interface CustomToolCallEvent extends ToolCallEventBase { + toolName: string; input: Record; } +/** Fired before a tool executes. Can block. */ +export type ToolCallEvent = + | BashToolCallEvent + | ReadToolCallEvent + | EditToolCallEvent + | WriteToolCallEvent + | GrepToolCallEvent + | FindToolCallEvent + | LsToolCallEvent + | CustomToolCallEvent; + interface ToolResultEventBase { type: "tool_result"; toolCallId: string; @@ -614,7 +669,7 @@ export type ToolResultEvent = | LsToolResultEvent | CustomToolResultEvent; -// Type guards +// Type guards for ToolResultEvent export function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent { return e.toolName === "bash"; } @@ -637,6 +692,41 @@ export function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent { return e.toolName === "ls"; } +/** + * Type guard for narrowing ToolCallEvent by tool name. + * + * Built-in tools narrow automatically (no type params needed): + * ```ts + * if (isToolCallEventType("bash", event)) { + * event.input.command; // string + * } + * ``` + * + * Custom tools require explicit type parameters: + * ```ts + * if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) { + * event.input.action; // typed + * } + * ``` + * + * Note: Direct narrowing via `event.toolName === "bash"` doesn't work because + * CustomToolCallEvent.toolName is `string` which overlaps with all literals. + */ +export function isToolCallEventType(toolName: "bash", event: ToolCallEvent): event is BashToolCallEvent; +export function isToolCallEventType(toolName: "read", event: ToolCallEvent): event is ReadToolCallEvent; +export function isToolCallEventType(toolName: "edit", event: ToolCallEvent): event is EditToolCallEvent; +export function isToolCallEventType(toolName: "write", event: ToolCallEvent): event is WriteToolCallEvent; +export function isToolCallEventType(toolName: "grep", event: ToolCallEvent): event is GrepToolCallEvent; +export function isToolCallEventType(toolName: "find", event: ToolCallEvent): event is FindToolCallEvent; +export function isToolCallEventType(toolName: "ls", event: ToolCallEvent): event is LsToolCallEvent; +export function isToolCallEventType>( + toolName: TName, + event: ToolCallEvent, +): event is ToolCallEvent & { toolName: TName; input: TInput }; +export function isToolCallEventType(toolName: string, event: ToolCallEvent): boolean { + return event.toolName === toolName; +} + /** Union of all event types */ export type ExtensionEvent = | ResourcesDiscoverEvent diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 3c312e52..9b166cca 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -3,7 +3,7 @@ import { createWriteStream, existsSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { AgentTool } from "@mariozechner/pi-agent-core"; -import { Type } from "@sinclair/typebox"; +import { type Static, Type } from "@sinclair/typebox"; import { spawn } from "child_process"; import { getShellConfig, getShellEnv, killProcessTree } from "../../utils/shell.js"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from "./truncate.js"; @@ -21,6 +21,8 @@ const bashSchema = Type.Object({ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })), }); +export type BashToolInput = Static; + export interface BashToolDetails { truncation?: TruncationResult; fullOutputPath?: string; diff --git a/packages/coding-agent/src/core/tools/edit.ts b/packages/coding-agent/src/core/tools/edit.ts index ecdc0ae6..1cfe8e2e 100644 --- a/packages/coding-agent/src/core/tools/edit.ts +++ b/packages/coding-agent/src/core/tools/edit.ts @@ -1,5 +1,5 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; -import { Type } from "@sinclair/typebox"; +import { type Static, Type } from "@sinclair/typebox"; import { constants } from "fs"; import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises"; import { @@ -19,6 +19,8 @@ const editSchema = Type.Object({ newText: Type.String({ description: "New text to replace the old text with" }), }); +export type EditToolInput = Static; + export interface EditToolDetails { /** Unified diff of the changes made */ diff: string; diff --git a/packages/coding-agent/src/core/tools/find.ts b/packages/coding-agent/src/core/tools/find.ts index a0a78bce..50004bff 100644 --- a/packages/coding-agent/src/core/tools/find.ts +++ b/packages/coding-agent/src/core/tools/find.ts @@ -1,5 +1,5 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; -import { Type } from "@sinclair/typebox"; +import { type Static, Type } from "@sinclair/typebox"; import { spawnSync } from "child_process"; import { existsSync } from "fs"; import { globSync } from "glob"; @@ -16,6 +16,8 @@ const findSchema = Type.Object({ limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 1000)" })), }); +export type FindToolInput = Static; + const DEFAULT_LIMIT = 1000; export interface FindToolDetails { diff --git a/packages/coding-agent/src/core/tools/grep.ts b/packages/coding-agent/src/core/tools/grep.ts index b0844817..61431912 100644 --- a/packages/coding-agent/src/core/tools/grep.ts +++ b/packages/coding-agent/src/core/tools/grep.ts @@ -1,6 +1,6 @@ import { createInterface } from "node:readline"; import type { AgentTool } from "@mariozechner/pi-agent-core"; -import { Type } from "@sinclair/typebox"; +import { type Static, Type } from "@sinclair/typebox"; import { spawn } from "child_process"; import { readFileSync, statSync } from "fs"; import path from "path"; @@ -29,6 +29,8 @@ const grepSchema = Type.Object({ limit: Type.Optional(Type.Number({ description: "Maximum number of matches to return (default: 100)" })), }); +export type GrepToolInput = Static; + const DEFAULT_LIMIT = 100; export interface GrepToolDetails { diff --git a/packages/coding-agent/src/core/tools/index.ts b/packages/coding-agent/src/core/tools/index.ts index 26328116..28687198 100644 --- a/packages/coding-agent/src/core/tools/index.ts +++ b/packages/coding-agent/src/core/tools/index.ts @@ -1,18 +1,48 @@ export { type BashOperations, type BashToolDetails, + type BashToolInput, type BashToolOptions, bashTool, createBashTool, } from "./bash.js"; -export { createEditTool, type EditOperations, type EditToolDetails, type EditToolOptions, editTool } from "./edit.js"; -export { createFindTool, type FindOperations, type FindToolDetails, type FindToolOptions, findTool } from "./find.js"; -export { createGrepTool, type GrepOperations, type GrepToolDetails, type GrepToolOptions, grepTool } from "./grep.js"; -export { createLsTool, type LsOperations, type LsToolDetails, type LsToolOptions, lsTool } from "./ls.js"; +export { + createEditTool, + type EditOperations, + type EditToolDetails, + type EditToolInput, + type EditToolOptions, + editTool, +} from "./edit.js"; +export { + createFindTool, + type FindOperations, + type FindToolDetails, + type FindToolInput, + type FindToolOptions, + findTool, +} from "./find.js"; +export { + createGrepTool, + type GrepOperations, + type GrepToolDetails, + type GrepToolInput, + type GrepToolOptions, + grepTool, +} from "./grep.js"; +export { + createLsTool, + type LsOperations, + type LsToolDetails, + type LsToolInput, + type LsToolOptions, + lsTool, +} from "./ls.js"; export { createReadTool, type ReadOperations, type ReadToolDetails, + type ReadToolInput, type ReadToolOptions, readTool, } from "./read.js"; @@ -26,7 +56,13 @@ export { truncateLine, truncateTail, } from "./truncate.js"; -export { createWriteTool, type WriteOperations, type WriteToolOptions, writeTool } from "./write.js"; +export { + createWriteTool, + type WriteOperations, + type WriteToolInput, + type WriteToolOptions, + writeTool, +} from "./write.js"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import { type BashToolOptions, bashTool, createBashTool } from "./bash.js"; diff --git a/packages/coding-agent/src/core/tools/ls.ts b/packages/coding-agent/src/core/tools/ls.ts index abd9b208..cc8117d8 100644 --- a/packages/coding-agent/src/core/tools/ls.ts +++ b/packages/coding-agent/src/core/tools/ls.ts @@ -1,5 +1,5 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; -import { Type } from "@sinclair/typebox"; +import { type Static, Type } from "@sinclair/typebox"; import { existsSync, readdirSync, statSync } from "fs"; import nodePath from "path"; import { resolveToCwd } from "./path-utils.js"; @@ -10,6 +10,8 @@ const lsSchema = Type.Object({ limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)" })), }); +export type LsToolInput = Static; + const DEFAULT_LIMIT = 500; export interface LsToolDetails { diff --git a/packages/coding-agent/src/core/tools/read.ts b/packages/coding-agent/src/core/tools/read.ts index f92fc1ac..c90a54ea 100644 --- a/packages/coding-agent/src/core/tools/read.ts +++ b/packages/coding-agent/src/core/tools/read.ts @@ -1,6 +1,6 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { ImageContent, TextContent } from "@mariozechner/pi-ai"; -import { Type } from "@sinclair/typebox"; +import { type Static, Type } from "@sinclair/typebox"; import { constants } from "fs"; import { access as fsAccess, readFile as fsReadFile } from "fs/promises"; import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js"; @@ -14,6 +14,8 @@ const readSchema = Type.Object({ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })), }); +export type ReadToolInput = Static; + export interface ReadToolDetails { truncation?: TruncationResult; } diff --git a/packages/coding-agent/src/core/tools/write.ts b/packages/coding-agent/src/core/tools/write.ts index a9248ca1..ccdae8a3 100644 --- a/packages/coding-agent/src/core/tools/write.ts +++ b/packages/coding-agent/src/core/tools/write.ts @@ -1,5 +1,5 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; -import { Type } from "@sinclair/typebox"; +import { type Static, Type } from "@sinclair/typebox"; import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises"; import { dirname } from "path"; import { resolveToCwd } from "./path-utils.js"; @@ -9,6 +9,8 @@ const writeSchema = Type.Object({ content: Type.String({ description: "Content to write to the file" }), }); +export type WriteToolInput = Static; + /** * Pluggable operations for the write tool. * Override these to delegate file writing to remote systems (e.g., SSH). diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 4e3ed05f..339ee7d4 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -46,10 +46,13 @@ export type { AgentToolResult, AgentToolUpdateCallback, AppAction, + BashToolCallEvent, BeforeAgentStartEvent, CompactOptions, ContextEvent, ContextUsage, + CustomToolCallEvent, + EditToolCallEvent, ExecOptions, ExecResult, Extension, @@ -69,15 +72,19 @@ export type { ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, + FindToolCallEvent, + GrepToolCallEvent, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, + LsToolCallEvent, MessageRenderer, MessageRenderOptions, ProviderConfig, ProviderModelConfig, + ReadToolCallEvent, RegisteredCommand, RegisteredTool, SessionBeforeCompactEvent, @@ -100,6 +107,7 @@ export type { UserBashEvent, UserBashEventResult, WidgetPlacement, + WriteToolCallEvent, } from "./core/extensions/index.js"; export { createExtensionRuntime, @@ -111,6 +119,7 @@ export { isGrepToolResult, isLsToolResult, isReadToolResult, + isToolCallEventType, isWriteToolResult, wrapRegisteredTool, wrapRegisteredTools, @@ -196,6 +205,7 @@ export { export { type BashOperations, type BashToolDetails, + type BashToolInput, type BashToolOptions, bashTool, codingTools, @@ -203,23 +213,28 @@ export { DEFAULT_MAX_LINES, type EditOperations, type EditToolDetails, + type EditToolInput, type EditToolOptions, editTool, type FindOperations, type FindToolDetails, + type FindToolInput, type FindToolOptions, findTool, formatSize, type GrepOperations, type GrepToolDetails, + type GrepToolInput, type GrepToolOptions, grepTool, type LsOperations, type LsToolDetails, + type LsToolInput, type LsToolOptions, lsTool, type ReadOperations, type ReadToolDetails, + type ReadToolInput, type ReadToolOptions, readTool, type ToolsOptions, @@ -229,6 +244,7 @@ export { truncateLine, truncateTail, type WriteOperations, + type WriteToolInput, type WriteToolOptions, writeTool, } from "./core/tools/index.js";