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
This commit is contained in:
Mario Zechner 2025-12-19 00:42:08 +01:00
parent 05b7b81338
commit 3d9bad8fb6
12 changed files with 187 additions and 34 deletions

View file

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

View file

@ -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<string, unknown>
// 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:

View file

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

View file

@ -44,26 +44,21 @@ export function wrapToolWithHooks<T>(tool: AgentTool<any, T>, 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,
};
}
}

View file

@ -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<string, unknown>;
/** 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;
}

View file

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

View file

@ -18,7 +18,7 @@ const findSchema = Type.Object({
const DEFAULT_LIMIT = 1000;
interface FindToolDetails {
export interface FindToolDetails {
truncation?: TruncationResult;
resultLimitReached?: number;
}

View file

@ -31,7 +31,7 @@ const grepSchema = Type.Object({
const DEFAULT_LIMIT = 100;
interface GrepToolDetails {
export interface GrepToolDetails {
truncation?: TruncationResult;
matchLimitReached?: number;
linesTruncated?: boolean;

View file

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

View file

@ -12,7 +12,7 @@ const lsSchema = Type.Object({
const DEFAULT_LIMIT = 500;
interface LsToolDetails {
export interface LsToolDetails {
truncation?: TruncationResult;
entryLimitReached?: number;
}

View file

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

View file

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