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 <noreply@anthropic.com>
This commit is contained in:
G 2026-02-01 15:19:24 +01:00 committed by Mario Zechner
parent 469fb5d27c
commit a8a0f4b9fb
13 changed files with 219 additions and 19 deletions

View file

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

View file

@ -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<string, unknown>;
}
/** 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<TName extends string, TInput extends Record<string, unknown>>(
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

View file

@ -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<typeof bashSchema>;
export interface BashToolDetails {
truncation?: TruncationResult;
fullOutputPath?: string;

View file

@ -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<typeof editSchema>;
export interface EditToolDetails {
/** Unified diff of the changes made */
diff: string;

View file

@ -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<typeof findSchema>;
const DEFAULT_LIMIT = 1000;
export interface FindToolDetails {

View file

@ -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<typeof grepSchema>;
const DEFAULT_LIMIT = 100;
export interface GrepToolDetails {

View file

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

View file

@ -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<typeof lsSchema>;
const DEFAULT_LIMIT = 500;
export interface LsToolDetails {

View file

@ -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<typeof readSchema>;
export interface ReadToolDetails {
truncation?: TruncationResult;
}

View file

@ -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<typeof writeSchema>;
/**
* Pluggable operations for the write tool.
* Override these to delegate file writing to remote systems (e.g., SSH).