feat(coding-agent): emit bash_execution_update events (#6971)

This commit is contained in:
Anantha Kumaran 2026-07-23 15:58:12 +05:30 • committed by GitHub
parent 7af8533c6f
commit fc85bdd88b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 68 additions and 9 deletions

View file

@ -23,7 +23,7 @@ Common options:
- **Responses**: JSON objects with `type: "response"` indicating command success/failure
- **Events**: Agent events streamed to stdout as JSON lines
All commands support an optional `id` field for request/response correlation. If provided, the corresponding response will include the same `id`.
All commands support an optional `id` field for request/response correlation. If provided, the corresponding response will include the same `id`. `bash_execution_update` events also include the `id` of their originating `bash` command.
### Framing
@ -455,15 +455,18 @@ Response:
#### bash
Execute a shell command and add output to conversation context.
Execute a shell command and add output to conversation context. Output streams as `bash_execution_update` events while the command runs; the response contains the final result.
```json
{"type": "bash", "command": "ls -la"}
{"id": "req-1", "type": "bash", "command": "ls -la"}
```
Include an `id` to associate streamed `bash_execution_update` events with this command.
Response:
```json
{
"id": "req-1",
"type": "response",
"command": "bash",
"success": true,
@ -494,7 +497,7 @@ If output was truncated, includes `fullOutputPath`:
**How bash results reach the LLM:**
The `bash` command executes immediately and returns a `BashResult`. Internally, a `BashExecutionMessage` is created and stored in the agent's message state. This message does NOT emit an event.
The `bash` command executes immediately and returns a `BashResult`. Internally, a `BashExecutionMessage` is created and stored in the agent's message state.
When the next `prompt` command is sent, all messages (including `BashExecutionMessage`) are transformed before being sent to the LLM. The `BashExecutionMessage` is converted to a `UserMessage` with this format:
@ -509,7 +512,6 @@ drwxr-xr-x ...
This means:
1. Bash output is included in the LLM context on the **next prompt**, not immediately
2. Multiple bash commands can be executed before a prompt; all outputs will be included
3. No event is emitted for the `BashExecutionMessage` itself
#### abort_bash
@ -829,7 +831,7 @@ Each command has:
## Events
Events are streamed to stdout as JSON lines during agent operation. Events do NOT include an `id` field (only responses do).
Events are streamed to stdout as JSON lines during agent operation. Events do not generally include an `id` field; `bash_execution_update` includes the `id` of its originating `bash` command when one was provided.
### Event Types
@ -843,6 +845,7 @@ Events are streamed to stdout as JSON lines during agent operation. Events do NO
| `message_start` | Message begins |
| `message_update` | Streaming update (text/thinking/toolcall deltas) |
| `message_end` | Message completes |
| `bash_execution_update` | Direct RPC bash command output chunk |
| `tool_execution_start` | Tool begins execution |
| `tool_execution_update` | Tool execution progress (streaming output) |
| `tool_execution_end` | Tool completes |
@ -951,6 +954,20 @@ Example streaming a text response:
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_end","contentIndex":0,"content":"Hello world","partial":{...}}}
```
### bash_execution_update
Emitted once for each output chunk from a direct `bash` command. `id` matches the command's `id`, allowing clients to associate output with the correct command.
Events stream all output while the command runs, even if the final `bash` response's `output` is truncated.
```json
{
"type": "bash_execution_update",
"id": "req-1",
"delta": "total 48\n"
}
```
### tool_execution_start / tool_execution_update / tool_execution_end
Emitted when a tool begins, streams progress, and completes execution.

View file

@ -176,7 +176,9 @@ export type AgentSessionEvent =
source: "compaction";
reason: "manual" | "threshold" | "overflow";
}
| { type: "summarization_retry_finished" };
| { type: "summarization_retry_finished" }
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
| { type: "bash_execution_update"; id?: string; delta: string };
/** Listener function for agent session events */
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
@ -2760,12 +2762,13 @@ export class AgentSession {
* @param command The bash command to execute
* @param onChunk Optional streaming callback for output
* @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)
* @param options.id Optional identifier included in bash execution update events
* @param options.operations Custom BashOperations for remote execution
*/
async executeBash(
command: string,
onChunk?: (chunk: string) => void,
options?: { excludeFromContext?: boolean; operations?: BashOperations },
options?: { excludeFromContext?: boolean; id?: string; operations?: BashOperations },
): Promise<BashResult> {
this._bashAbortController = new AbortController();
@ -2780,7 +2783,10 @@ export class AgentSession {
this.sessionManager.getCwd(),
options?.operations ?? createLocalBashOperations({ shellPath }),
{
onChunk,
onChunk: (delta) => {
onChunk?.(delta);
this._emit({ type: "bash_execution_update", id: options?.id, delta });
},
signal: this._bashAbortController.signal,
},
);

View file

@ -2985,6 +2985,10 @@ export class InteractiveMode {
this.ui.requestRender();
break;
case "bash_execution_update":
// The bash execution callback handles TUI output rendering.
break;
case "tool_execution_start": {
let component = this.pendingTools.get(event.toolCallId);
if (!component) {

View file

@ -558,6 +558,7 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
case "bash": {
const result = await session.executeBash(command.command, undefined, {
excludeFromContext: command.excludeFromContext,
id,
});
return success(id, "bash", result);
}

View file

@ -239,4 +239,35 @@ describe("AgentSession bash and persistence characterization", () => {
expect(result.output).toContain("hello from custom ops");
expect(harness.session.messages[harness.session.messages.length - 1]?.role).toBe("bashExecution");
});
it("streams bash output to the callback and session events", async () => {
const harness = await createHarness();
harnesses.push(harness);
const callbackDeltas: string[] = [];
const eventUpdates: Array<{ id: string | undefined; delta: string }> = [];
const unsubscribe = harness.session.subscribe((event) => {
if (event.type === "bash_execution_update") {
eventUpdates.push({ id: event.id, delta: event.delta });
}
});
const operations: BashOperations = {
exec: async (_command, _cwd, options) => {
options.onData(Buffer.from("hello "));
options.onData(Buffer.from("world"));
return { exitCode: 0 };
},
};
await harness.session.executeBash("custom", (delta) => callbackDeltas.push(delta), {
id: "bash-1",
operations,
});
unsubscribe();
expect(callbackDeltas).toEqual(["hello ", "world"]);
expect(eventUpdates).toEqual([
{ id: "bash-1", delta: "hello " },
{ id: "bash-1", delta: "world" },
]);
});
});