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

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