mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 04:02:35 +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
|
|
@ -2,8 +2,14 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **Streaming bash output**: Bash tool now streams output in real-time during execution. The TUI displays live progress with the last 5 lines visible (expandable with ctrl+o). ([#44](https://github.com/badlogic/pi-mono/issues/44))
|
||||
|
||||
### Changed
|
||||
|
||||
- **Tool output display**: When collapsed, tool output now shows the last N lines instead of the first N lines, making streaming output more useful.
|
||||
|
||||
- Updated `@mariozechner/pi-ai` with X-Initiator header support for GitHub Copilot, ensuring agent calls are not deducted from quota. ([#200](https://github.com/badlogic/pi-mono/pull/200) by [@kim0](https://github.com/kim0))
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -553,6 +553,7 @@ Events are streamed to stdout as JSON lines during agent operation. Events do NO
|
|||
| `message_update` | Streaming update (text/thinking/toolcall deltas) |
|
||||
| `message_end` | Message completes |
|
||||
| `tool_execution_start` | Tool begins execution |
|
||||
| `tool_execution_update` | Tool execution progress (streaming output) |
|
||||
| `tool_execution_end` | Tool completes |
|
||||
| `auto_compaction_start` | Auto-compaction begins |
|
||||
| `auto_compaction_end` | Auto-compaction completes |
|
||||
|
|
@ -645,9 +646,9 @@ Example streaming a text response:
|
|||
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_end","contentIndex":0,"content":"Hello world","partial":{...}}}
|
||||
```
|
||||
|
||||
### tool_execution_start / tool_execution_end
|
||||
### tool_execution_start / tool_execution_update / tool_execution_end
|
||||
|
||||
Emitted when a tool begins and completes execution.
|
||||
Emitted when a tool begins, streams progress, and completes execution.
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -658,6 +659,23 @@ Emitted when a tool begins and completes execution.
|
|||
}
|
||||
```
|
||||
|
||||
During execution, `tool_execution_update` events stream partial results (e.g., bash output as it arrives):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tool_execution_update",
|
||||
"toolCallId": "call_abc123",
|
||||
"toolName": "bash",
|
||||
"args": {"command": "ls -la"},
|
||||
"partialResult": {
|
||||
"content": [{"type": "text", "text": "partial output so far..."}],
|
||||
"details": {"truncation": null, "fullOutputPath": null}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When complete:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tool_execution_end",
|
||||
|
|
@ -671,7 +689,7 @@ Emitted when a tool begins and completes execution.
|
|||
}
|
||||
```
|
||||
|
||||
Use `toolCallId` to correlate `tool_execution_start` with `tool_execution_end`.
|
||||
Use `toolCallId` to correlate events. The `partialResult` in `tool_execution_update` contains the accumulated output so far (not just the delta), allowing clients to simply replace their display on each update.
|
||||
|
||||
### auto_compaction_start / auto_compaction_end
|
||||
|
||||
|
|
|
|||
|
|
@ -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