From 3d9bad8fb67f1435a707ac42ba73d8e804499304 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 19 Dec 2025 00:42:08 +0100 Subject: [PATCH] Expose full tool result content and details in hook tool_result event Breaking change: ToolResultEvent now exposes content and typed details instead of just a result string. Hook handlers returning { result: ... } must change to { content: [...] }. - ToolResultEvent is now a discriminated union based on toolName - Each built-in tool has typed details (BashToolDetails, etc.) - Export tool details types and TruncationResult - Update hooks.md documentation Closes #233 --- packages/coding-agent/CHANGELOG.md | 10 +++ packages/coding-agent/docs/hooks.md | 53 ++++++++++- packages/coding-agent/src/core/hooks/index.ts | 8 ++ .../src/core/hooks/tool-wrapper.ts | 15 ++-- packages/coding-agent/src/core/hooks/types.ts | 89 ++++++++++++++++--- packages/coding-agent/src/core/tools/bash.ts | 2 +- packages/coding-agent/src/core/tools/find.ts | 2 +- packages/coding-agent/src/core/tools/grep.ts | 2 +- packages/coding-agent/src/core/tools/index.ts | 11 +-- packages/coding-agent/src/core/tools/ls.ts | 2 +- packages/coding-agent/src/core/tools/read.ts | 2 +- packages/coding-agent/src/index.ts | 25 +++++- 12 files changed, 187 insertions(+), 34 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d92471b6..104e8da5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,16 @@ ## [Unreleased] +### Breaking Changes + +- **Hook `tool_result` event restructured**: The `ToolResultEvent` now exposes full tool result data instead of just text. ([#233](https://github.com/badlogic/pi-mono/pull/233)) + - Removed: `result: string` field + - Added: `content: (TextContent | ImageContent)[]` - full content array + - Added: `details: unknown` - tool-specific details (typed per tool via discriminated union on `toolName`) + - `ToolResultEventResult.result` renamed to `ToolResultEventResult.text` (removed), use `content` instead + - Hook handlers returning `{ result: "..." }` must change to `{ content: [{ type: "text", text: "..." }] }` + - Built-in tool details types exported: `BashToolDetails`, `ReadToolDetails`, `GrepToolDetails`, `FindToolDetails`, `LsToolDetails`, `TruncationResult` + ### Changed - **Skills standard compliance**: Skills now adhere to the [Agent Skills standard](https://agentskills.io/specification). Validates name (must match parent directory, lowercase, max 64 chars), description (required, max 1024 chars), and frontmatter fields. Warns on violations but remains lenient. Prompt format changed to XML structure. Removed `{baseDir}` placeholder in favor of relative paths. ([#231](https://github.com/badlogic/pi-mono/issues/231)) diff --git a/packages/coding-agent/docs/hooks.md b/packages/coding-agent/docs/hooks.md index 6fd76869..6d73ded1 100644 --- a/packages/coding-agent/docs/hooks.md +++ b/packages/coding-agent/docs/hooks.md @@ -222,13 +222,60 @@ Fired after tool executes. **Can modify result.** ```typescript pi.on("tool_result", async (event, ctx) => { - // event.toolName, event.toolCallId, event.input - // event.result: string + // event.toolName: string + // event.toolCallId: string + // event.input: Record + // event.content: (TextContent | ImageContent)[] + // event.details: tool-specific (see below) // event.isError: boolean - return { result: "modified" }; // or undefined to keep original + + // Return modified content/details, or undefined to keep original + return { content: [...], details: {...} }; }); ``` +The event type is a discriminated union based on `toolName`. TypeScript will narrow `details` to the correct type: + +```typescript +pi.on("tool_result", async (event, ctx) => { + if (event.toolName === "bash") { + // event.details is BashToolDetails | undefined + if (event.details?.truncation?.truncated) { + // Access full output from temp file + const fullPath = event.details.fullOutputPath; + } + } +}); +``` + +#### Tool Details Types + +Each built-in tool has a typed `details` field. Types are exported from `@mariozechner/pi-coding-agent`: + +| Tool | Details Type | Source | +|------|-------------|--------| +| `bash` | `BashToolDetails` | `src/core/tools/bash.ts` | +| `read` | `ReadToolDetails` | `src/core/tools/read.ts` | +| `edit` | `undefined` | - | +| `write` | `undefined` | - | +| `grep` | `GrepToolDetails` | `src/core/tools/grep.ts` | +| `find` | `FindToolDetails` | `src/core/tools/find.ts` | +| `ls` | `LsToolDetails` | `src/core/tools/ls.ts` | + +Common fields in details: +- `truncation?: TruncationResult` - present when output was truncated +- `fullOutputPath?: string` - path to temp file with full output (bash only) + +`TruncationResult` contains: +- `truncated: boolean` - whether truncation occurred +- `truncatedBy: "lines" | "bytes" | null` - which limit was hit +- `totalLines`, `totalBytes` - original size +- `outputLines`, `outputBytes` - truncated size + +Custom tools use `CustomToolResultEvent` with `details: unknown`. + +**Note:** If you modify `content`, you should also update `details` accordingly. The TUI uses `details` (e.g., truncation info) for rendering, so inconsistent values will cause display issues. + ## Context API Every event handler receives a context object with these methods: diff --git a/packages/coding-agent/src/core/hooks/index.ts b/packages/coding-agent/src/core/hooks/index.ts index 739603e1..b8335913 100644 --- a/packages/coding-agent/src/core/hooks/index.ts +++ b/packages/coding-agent/src/core/hooks/index.ts @@ -4,15 +4,22 @@ export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper.js"; export type { AgentEndEvent, AgentStartEvent, + BashToolResultEvent, BranchEvent, BranchEventResult, + CustomToolResultEvent, + EditToolResultEvent, ExecResult, + FindToolResultEvent, + GrepToolResultEvent, HookAPI, HookError, HookEvent, HookEventContext, HookFactory, HookUIContext, + LsToolResultEvent, + ReadToolResultEvent, SessionEvent, ToolCallEvent, ToolCallEventResult, @@ -20,4 +27,5 @@ export type { ToolResultEventResult, TurnEndEvent, TurnStartEvent, + WriteToolResultEvent, } from "./types.js"; diff --git a/packages/coding-agent/src/core/hooks/tool-wrapper.ts b/packages/coding-agent/src/core/hooks/tool-wrapper.ts index f4eb6b07..a36fe8d6 100644 --- a/packages/coding-agent/src/core/hooks/tool-wrapper.ts +++ b/packages/coding-agent/src/core/hooks/tool-wrapper.ts @@ -44,26 +44,21 @@ export function wrapToolWithHooks(tool: AgentTool, hookRunner: HookRu // Emit tool_result event - hooks can modify the result if (hookRunner.hasHandlers("tool_result")) { - // Extract text from result for hooks - const resultText = result.content - .filter((c): c is { type: "text"; text: string } => c.type === "text") - .map((c) => c.text) - .join("\n"); - const resultResult = (await hookRunner.emit({ type: "tool_result", toolName: tool.name, toolCallId, input: params, - result: resultText, + content: result.content, + details: result.details, isError: false, })) as ToolResultEventResult | undefined; // Apply modifications if any - if (resultResult?.result !== undefined) { + if (resultResult) { return { - ...result, - content: [{ type: "text", text: resultResult.result }], + content: resultResult.content ?? result.content, + details: (resultResult.details ?? result.details) as T, }; } } diff --git a/packages/coding-agent/src/core/hooks/types.ts b/packages/coding-agent/src/core/hooks/types.ts index b47e1857..e1677fbc 100644 --- a/packages/coding-agent/src/core/hooks/types.ts +++ b/packages/coding-agent/src/core/hooks/types.ts @@ -6,8 +6,15 @@ */ import type { AppMessage, Attachment } from "@mariozechner/pi-agent-core"; -import type { ToolResultMessage } from "@mariozechner/pi-ai"; +import type { ImageContent, TextContent, ToolResultMessage } from "@mariozechner/pi-ai"; import type { SessionEntry } from "../session-manager.js"; +import type { + BashToolDetails, + FindToolDetails, + GrepToolDetails, + LsToolDetails, + ReadToolDetails, +} from "../tools/index.js"; // ============================================================================ // Execution Context @@ -140,23 +147,83 @@ export interface ToolCallEvent { } /** - * Event data for tool_result event. - * Fired after a tool is executed. Hooks can modify the result. + * Base interface for tool_result events. */ -export interface ToolResultEvent { +interface ToolResultEventBase { type: "tool_result"; - /** Tool name (e.g., "bash", "edit", "write") */ - toolName: string; /** Tool call ID */ toolCallId: string; /** Tool input parameters */ input: Record; - /** Tool result content (text) */ - result: string; + /** Full content array (text and images) */ + content: (TextContent | ImageContent)[]; /** Whether the tool execution was an error */ isError: boolean; } +/** Tool result event for bash tool */ +export interface BashToolResultEvent extends ToolResultEventBase { + toolName: "bash"; + details: BashToolDetails | undefined; +} + +/** Tool result event for read tool */ +export interface ReadToolResultEvent extends ToolResultEventBase { + toolName: "read"; + details: ReadToolDetails | undefined; +} + +/** Tool result event for edit tool */ +export interface EditToolResultEvent extends ToolResultEventBase { + toolName: "edit"; + details: undefined; +} + +/** Tool result event for write tool */ +export interface WriteToolResultEvent extends ToolResultEventBase { + toolName: "write"; + details: undefined; +} + +/** Tool result event for grep tool */ +export interface GrepToolResultEvent extends ToolResultEventBase { + toolName: "grep"; + details: GrepToolDetails | undefined; +} + +/** Tool result event for find tool */ +export interface FindToolResultEvent extends ToolResultEventBase { + toolName: "find"; + details: FindToolDetails | undefined; +} + +/** Tool result event for ls tool */ +export interface LsToolResultEvent extends ToolResultEventBase { + toolName: "ls"; + details: LsToolDetails | undefined; +} + +/** Tool result event for custom/unknown tools */ +export interface CustomToolResultEvent extends ToolResultEventBase { + toolName: string; + details: unknown; +} + +/** + * Event data for tool_result event. + * Fired after a tool is executed. Hooks can modify the result. + * Use toolName to discriminate and get typed details. + */ +export type ToolResultEvent = + | BashToolResultEvent + | ReadToolResultEvent + | EditToolResultEvent + | WriteToolResultEvent + | GrepToolResultEvent + | FindToolResultEvent + | LsToolResultEvent + | CustomToolResultEvent; + /** * Event data for branch event. */ @@ -201,8 +268,10 @@ export interface ToolCallEventResult { * Allows hooks to modify tool results. */ export interface ToolResultEventResult { - /** Modified result text (if not set, original result is used) */ - result?: string; + /** Replacement content array (text and images) */ + content?: (TextContent | ImageContent)[]; + /** Replacement details */ + details?: unknown; /** Override isError flag */ isError?: boolean; } diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index f109d2ec..df69ae53 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -21,7 +21,7 @@ const bashSchema = Type.Object({ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })), }); -interface BashToolDetails { +export interface BashToolDetails { truncation?: TruncationResult; fullOutputPath?: string; } diff --git a/packages/coding-agent/src/core/tools/find.ts b/packages/coding-agent/src/core/tools/find.ts index de15513a..c241e3b4 100644 --- a/packages/coding-agent/src/core/tools/find.ts +++ b/packages/coding-agent/src/core/tools/find.ts @@ -18,7 +18,7 @@ const findSchema = Type.Object({ const DEFAULT_LIMIT = 1000; -interface FindToolDetails { +export interface FindToolDetails { truncation?: TruncationResult; resultLimitReached?: number; } diff --git a/packages/coding-agent/src/core/tools/grep.ts b/packages/coding-agent/src/core/tools/grep.ts index 73be92ff..7e75178e 100644 --- a/packages/coding-agent/src/core/tools/grep.ts +++ b/packages/coding-agent/src/core/tools/grep.ts @@ -31,7 +31,7 @@ const grepSchema = Type.Object({ const DEFAULT_LIMIT = 100; -interface GrepToolDetails { +export interface GrepToolDetails { truncation?: TruncationResult; matchLimitReached?: number; linesTruncated?: boolean; diff --git a/packages/coding-agent/src/core/tools/index.ts b/packages/coding-agent/src/core/tools/index.ts index 94af0286..034f554a 100644 --- a/packages/coding-agent/src/core/tools/index.ts +++ b/packages/coding-agent/src/core/tools/index.ts @@ -1,9 +1,10 @@ -export { bashTool } from "./bash.js"; +export { type BashToolDetails, bashTool } from "./bash.js"; export { editTool } from "./edit.js"; -export { findTool } from "./find.js"; -export { grepTool } from "./grep.js"; -export { lsTool } from "./ls.js"; -export { readTool } from "./read.js"; +export { type FindToolDetails, findTool } from "./find.js"; +export { type GrepToolDetails, grepTool } from "./grep.js"; +export { type LsToolDetails, lsTool } from "./ls.js"; +export { type ReadToolDetails, readTool } from "./read.js"; +export type { TruncationResult } from "./truncate.js"; export { writeTool } from "./write.js"; import { bashTool } from "./bash.js"; diff --git a/packages/coding-agent/src/core/tools/ls.ts b/packages/coding-agent/src/core/tools/ls.ts index 2be75755..3edfcffe 100644 --- a/packages/coding-agent/src/core/tools/ls.ts +++ b/packages/coding-agent/src/core/tools/ls.ts @@ -12,7 +12,7 @@ const lsSchema = Type.Object({ const DEFAULT_LIMIT = 500; -interface LsToolDetails { +export interface LsToolDetails { truncation?: TruncationResult; entryLimitReached?: number; } diff --git a/packages/coding-agent/src/core/tools/read.ts b/packages/coding-agent/src/core/tools/read.ts index 678b4535..4b83534b 100644 --- a/packages/coding-agent/src/core/tools/read.ts +++ b/packages/coding-agent/src/core/tools/read.ts @@ -13,7 +13,7 @@ const readSchema = Type.Object({ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })), }); -interface ReadToolDetails { +export interface ReadToolDetails { truncation?: TruncationResult; } diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index b4c313b0..7184d4cd 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -39,13 +39,20 @@ export { discoverAndLoadCustomTools, loadCustomTools } from "./core/custom-tools export type { AgentEndEvent, AgentStartEvent, + BashToolResultEvent, BranchEvent, BranchEventResult, + CustomToolResultEvent, + EditToolResultEvent, + FindToolResultEvent, + GrepToolResultEvent, HookAPI, HookEvent, HookEventContext, HookFactory, HookUIContext, + LsToolResultEvent, + ReadToolResultEvent, SessionEvent, ToolCallEvent, ToolCallEventResult, @@ -53,6 +60,7 @@ export type { ToolResultEventResult, TurnEndEvent, TurnStartEvent, + WriteToolResultEvent, } from "./core/hooks/index.js"; export { messageTransformer } from "./core/messages.js"; export { @@ -89,7 +97,22 @@ export { type SkillWarning, } from "./core/skills.js"; // Tools -export { bashTool, codingTools, editTool, readTool, writeTool } from "./core/tools/index.js"; +export { + type BashToolDetails, + bashTool, + codingTools, + editTool, + type FindToolDetails, + findTool, + type GrepToolDetails, + grepTool, + type LsToolDetails, + lsTool, + type ReadToolDetails, + readTool, + type TruncationResult, + writeTool, +} from "./core/tools/index.js"; // Main entry point export { main } from "./main.js";