mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 06:04:44 +00:00
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:
parent
8319628bc3
commit
7ac832586f
12 changed files with 362 additions and 51 deletions
|
|
@ -4,8 +4,14 @@
|
|||
|
||||
### Added
|
||||
|
||||
- **Tool result streaming**: Added `tool_execution_update` event and optional `onUpdate` callback to `AgentTool.execute()` for streaming tool output during execution. Tools can now emit partial results (e.g., bash stdout) that are forwarded to subscribers. ([#44](https://github.com/badlogic/pi-mono/issues/44))
|
||||
|
||||
- **X-Initiator header for GitHub Copilot**: Added X-Initiator header handling for GitHub Copilot provider to ensure correct call accounting (agent calls are not deducted from quota). Sets initiator based on last message role. ([#200](https://github.com/badlogic/pi-mono/pull/200) by [@kim0](https://github.com/kim0))
|
||||
|
||||
### Changed
|
||||
|
||||
- **Normalized tool_execution_end result**: `tool_execution_end` event now always contains `AgentToolResult` (no longer `AgentToolResult | string`). Errors are wrapped in the standard result format.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Reasoning disabled by default**: When `reasoning` option is not specified, thinking is now explicitly disabled for all providers. Previously, some providers like Gemini with "dynamic thinking" would use their default (thinking ON), causing unexpected token usage. This was the original intended behavior. ([#180](https://github.com/badlogic/pi-mono/pull/180) by [@markusylisiurunen](https://github.com/markusylisiurunen))
|
||||
|
|
|
|||
|
|
@ -822,10 +822,12 @@ const stream = agentLoop(
|
|||
// 5. message_start - Assistant message starts
|
||||
// 6. message_update - Assistant streams response with tool calls
|
||||
// 7. message_end - Assistant message ends
|
||||
// 8. tool_execution_start - First calculation (15 * 20)
|
||||
// 9. tool_execution_end - Result: 300
|
||||
// 10. tool_execution_start - Second calculation (30 * 40)
|
||||
// 11. tool_execution_end - Result: 1200
|
||||
// 8. tool_execution_start - First calculation (15 * 20)
|
||||
// 9. tool_execution_update - Streaming progress (for long-running tools)
|
||||
// 10. tool_execution_end - Result: 300
|
||||
// 11. tool_execution_start - Second calculation (30 * 40)
|
||||
// 12. tool_execution_update - Streaming progress
|
||||
// 13. tool_execution_end - Result: 1200
|
||||
// 12. message_start - Tool result message for first calculation
|
||||
// 13. message_end - Tool result message ends
|
||||
// 14. message_start - Tool result message for second calculation
|
||||
|
|
@ -876,11 +878,16 @@ for await (const event of stream) {
|
|||
console.log(`Calling ${event.toolName} with:`, event.args);
|
||||
break;
|
||||
|
||||
case 'tool_execution_update':
|
||||
// Streaming progress for long-running tools (e.g., bash output)
|
||||
console.log(`Progress:`, event.partialResult.content);
|
||||
break;
|
||||
|
||||
case 'tool_execution_end':
|
||||
if (event.isError) {
|
||||
console.error(`Tool failed:`, event.result);
|
||||
} else {
|
||||
console.log(`Tool result:`, event.result.output);
|
||||
console.log(`Tool result:`, event.result.content);
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -947,11 +954,13 @@ const weatherTool: AgentTool<typeof weatherSchema, { temp: number }> = {
|
|||
name: 'get_weather',
|
||||
description: 'Get current weather for a city',
|
||||
parameters: weatherSchema,
|
||||
execute: async (toolCallId, args) => {
|
||||
execute: async (toolCallId, args, signal, onUpdate) => {
|
||||
// args is fully typed: { city: string, units: 'celsius' | 'fahrenheit' }
|
||||
// signal: AbortSignal for cancellation
|
||||
// onUpdate: Optional callback for streaming progress (emits tool_execution_update events)
|
||||
const temp = Math.round(Math.random() * 30);
|
||||
return {
|
||||
output: `Temperature in ${args.city}: ${temp}°${args.units[0].toUpperCase()}`,
|
||||
content: [{ type: 'text', text: `Temperature in ${args.city}: ${temp}°${args.units[0].toUpperCase()}` }],
|
||||
details: { temp }
|
||||
};
|
||||
}
|
||||
|
|
@ -973,6 +982,36 @@ const chartTool: AgentTool<typeof Type.Object({ data: Type.Array(Type.Number())
|
|||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Tools can stream progress via the onUpdate callback (emits tool_execution_update events)
|
||||
const bashTool: AgentTool<typeof Type.Object({ command: Type.String() }), { exitCode: number }> = {
|
||||
label: 'Run Bash',
|
||||
name: 'bash',
|
||||
description: 'Execute a bash command',
|
||||
parameters: Type.Object({ command: Type.String() }),
|
||||
execute: async (toolCallId, args, signal, onUpdate) => {
|
||||
let output = '';
|
||||
const child = spawn('bash', ['-c', args.command]);
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
// Stream partial output to UI via tool_execution_update events
|
||||
onUpdate?.({
|
||||
content: [{ type: 'text', text: output }],
|
||||
details: { exitCode: -1 } // Not finished yet
|
||||
});
|
||||
});
|
||||
|
||||
const exitCode = await new Promise<number>((resolve) => {
|
||||
child.on('close', resolve);
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: output }],
|
||||
details: { exitCode }
|
||||
};
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Validation and Error Handling
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue