Add tool result streaming

- Add AgentToolUpdateCallback type and optional onUpdate callback to AgentTool.execute()
- Add tool_execution_update event with toolCallId, toolName, args, partialResult
- Normalize tool_execution_end to always use AgentToolResult (no more string fallback)
- Bash tool streams truncated rolling buffer output during execution
- ToolExecutionComponent shows last N lines when collapsed (not first N)
- Interactive mode handles tool_execution_update events
- Update RPC docs and ai/agent READMEs

fixes #44
This commit is contained in:
Mario Zechner 2025-12-16 14:53:17 +01:00
parent 8319628bc3
commit 7ac832586f
12 changed files with 362 additions and 51 deletions

View file

@ -243,7 +243,7 @@ async function executeToolCalls<T>(
args: toolCall.arguments,
});
let resultOrError: AgentToolResult<T> | string;
let result: AgentToolResult<T>;
let isError = false;
try {
@ -252,10 +252,21 @@ async function executeToolCalls<T>(
// Validate arguments using shared validation function
const validatedArgs = validateToolArguments(tool, toolCall);
// Execute with validated, typed arguments
resultOrError = await tool.execute(toolCall.id, validatedArgs, signal);
// Execute with validated, typed arguments, passing update callback
result = await tool.execute(toolCall.id, validatedArgs, signal, (partialResult) => {
stream.push({
type: "tool_execution_update",
toolCallId: toolCall.id,
toolName: toolCall.name,
args: toolCall.arguments,
partialResult,
});
});
} catch (e) {
resultOrError = e instanceof Error ? e.message : String(e);
result = {
content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
details: {} as T,
};
isError = true;
}
@ -263,20 +274,16 @@ async function executeToolCalls<T>(
type: "tool_execution_end",
toolCallId: toolCall.id,
toolName: toolCall.name,
result: resultOrError,
result,
isError,
});
// Convert result to content blocks
const content: ToolResultMessage<T>["content"] =
typeof resultOrError === "string" ? [{ type: "text", text: resultOrError }] : resultOrError.content;
const toolResultMessage: ToolResultMessage<T> = {
role: "toolResult",
toolCallId: toolCall.id,
toolName: toolCall.name,
content,
details: typeof resultOrError === "string" ? ({} as T) : resultOrError.details,
content: result.content,
details: result.details,
isError,
timestamp: Date.now(),
};

View file

@ -1,3 +1,11 @@
export { agentLoop, agentLoopContinue } from "./agent-loop.js";
export * from "./tools/index.js";
export type { AgentContext, AgentEvent, AgentLoopConfig, AgentTool, AgentToolResult, QueuedMessage } from "./types.js";
export type {
AgentContext,
AgentEvent,
AgentLoopConfig,
AgentTool,
AgentToolResult,
AgentToolUpdateCallback,
QueuedMessage,
} from "./types.js";

View file

@ -18,6 +18,9 @@ export interface AgentToolResult<T> {
details: T;
}
// Callback for streaming tool execution updates
export type AgentToolUpdateCallback<T = any> = (partialResult: AgentToolResult<T>) => void;
// AgentTool extends Tool but adds the execute function
export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any> extends Tool<TParameters> {
// A human-readable label for the tool to be displayed in UI
@ -26,6 +29,7 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
toolCallId: string,
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>,
) => Promise<AgentToolResult<TDetails>>;
}
@ -50,12 +54,20 @@ export type AgentEvent =
| { type: "message_end"; message: Message }
// Emitted when a tool execution starts
| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any }
// Emitted when a tool execution produces output (streaming)
| {
type: "tool_execution_update";
toolCallId: string;
toolName: string;
args: any;
partialResult: AgentToolResult<any>;
}
// Emitted when a tool execution completes
| {
type: "tool_execution_end";
toolCallId: string;
toolName: string;
result: AgentToolResult<any> | string;
result: AgentToolResult<any>;
isError: boolean;
}
// Emitted when a full turn completes