mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 04:01:56 +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
|
|
@ -35,6 +35,7 @@ export const bashTool: AgentTool<typeof bashSchema> = {
|
|||
_toolCallId: string,
|
||||
{ command, timeout }: { command: string; timeout?: number },
|
||||
signal?: AbortSignal,
|
||||
onUpdate?,
|
||||
) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { shell, args } = getShellConfig();
|
||||
|
|
@ -92,6 +93,20 @@ export const bashTool: AgentTool<typeof bashSchema> = {
|
|||
const removed = chunks.shift()!;
|
||||
chunksBytes -= removed.length;
|
||||
}
|
||||
|
||||
// Stream partial output to callback (truncated rolling buffer)
|
||||
if (onUpdate) {
|
||||
const fullBuffer = Buffer.concat(chunks);
|
||||
const fullText = fullBuffer.toString("utf-8");
|
||||
const truncation = truncateTail(fullText);
|
||||
onUpdate({
|
||||
content: [{ type: "text", text: truncation.content || "" }],
|
||||
details: {
|
||||
truncation: truncation.truncated ? truncation : undefined,
|
||||
fullOutputPath: tempFilePath,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Collect stdout and stderr together
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export class ToolExecutionComponent extends Container {
|
|||
private args: any;
|
||||
private expanded = false;
|
||||
private showImages: boolean;
|
||||
private isPartial = false;
|
||||
private result?: {
|
||||
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
||||
isError: boolean;
|
||||
|
|
@ -66,12 +67,16 @@ export class ToolExecutionComponent extends Container {
|
|||
this.updateDisplay();
|
||||
}
|
||||
|
||||
updateResult(result: {
|
||||
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
||||
details?: any;
|
||||
isError: boolean;
|
||||
}): void {
|
||||
updateResult(
|
||||
result: {
|
||||
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
||||
details?: any;
|
||||
isError: boolean;
|
||||
},
|
||||
isPartial = false,
|
||||
): void {
|
||||
this.result = result;
|
||||
this.isPartial = isPartial;
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
|
|
@ -86,11 +91,11 @@ export class ToolExecutionComponent extends Container {
|
|||
}
|
||||
|
||||
private updateDisplay(): void {
|
||||
const bgFn = this.result
|
||||
? this.result.isError
|
||||
const bgFn = this.isPartial
|
||||
? (text: string) => theme.bg("toolPendingBg", text)
|
||||
: this.result?.isError
|
||||
? (text: string) => theme.bg("toolErrorBg", text)
|
||||
: (text: string) => theme.bg("toolSuccessBg", text)
|
||||
: (text: string) => theme.bg("toolPendingBg", text);
|
||||
: (text: string) => theme.bg("toolSuccessBg", text);
|
||||
|
||||
this.contentText.setCustomBgFn(bgFn);
|
||||
this.contentText.setText(this.formatToolExecution());
|
||||
|
|
@ -164,13 +169,15 @@ export class ToolExecutionComponent extends Container {
|
|||
if (output) {
|
||||
const lines = output.split("\n");
|
||||
const maxLines = this.expanded ? lines.length : 5;
|
||||
const displayLines = lines.slice(0, maxLines);
|
||||
const remaining = lines.length - maxLines;
|
||||
const skipped = Math.max(0, lines.length - maxLines);
|
||||
const displayLines = lines.slice(-maxLines);
|
||||
|
||||
text += "\n\n" + displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n");
|
||||
if (remaining > 0) {
|
||||
text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
|
||||
if (skipped > 0) {
|
||||
text += theme.fg("toolOutput", `\n\n... (${skipped} earlier lines)`);
|
||||
}
|
||||
text +=
|
||||
(skipped > 0 ? "\n" : "\n\n") +
|
||||
displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n");
|
||||
}
|
||||
|
||||
// Show truncation warning at the bottom (outside collapsed area)
|
||||
|
|
|
|||
|
|
@ -755,18 +755,19 @@ export class InteractiveMode {
|
|||
break;
|
||||
}
|
||||
|
||||
case "tool_execution_update": {
|
||||
const component = this.pendingTools.get(event.toolCallId);
|
||||
if (component) {
|
||||
component.updateResult({ ...event.partialResult, isError: false }, true);
|
||||
this.ui.requestRender();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "tool_execution_end": {
|
||||
const component = this.pendingTools.get(event.toolCallId);
|
||||
if (component) {
|
||||
const resultData =
|
||||
typeof event.result === "string"
|
||||
? {
|
||||
content: [{ type: "text" as const, text: event.result }],
|
||||
details: undefined,
|
||||
isError: event.isError,
|
||||
}
|
||||
: { content: event.result.content, details: event.result.details, isError: event.isError };
|
||||
component.updateResult(resultData);
|
||||
component.updateResult({ ...event.result, isError: event.isError });
|
||||
this.pendingTools.delete(event.toolCallId);
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
|
@ -993,11 +994,7 @@ export class InteractiveMode {
|
|||
} else if (message.role === "toolResult") {
|
||||
const component = this.pendingTools.get(message.toolCallId);
|
||||
if (component) {
|
||||
component.updateResult({
|
||||
content: message.content,
|
||||
details: message.details,
|
||||
isError: message.isError,
|
||||
});
|
||||
component.updateResult(message);
|
||||
this.pendingTools.delete(message.toolCallId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue