Explorar o código

feat(cli): include exitCode in command tool_result events (#11820)

Propagate the command exit code through the JSON event emitter so CLI
consumers can distinguish between successful and failed command
executions without parsing output text.

Co-authored-by: Claude Opus 4.6 <[email protected]>
Chris Estreich hai 1 mes
pai
achega
eb2147148e

+ 2 - 2
apps/cli/src/agent/__tests__/json-event-emitter-streaming.test.ts

@@ -302,7 +302,7 @@ describe("JsonEventEmitter streaming deltas", () => {
 
 		emitter.emitCommandOutputChunk("line1\n")
 		emitter.emitCommandOutputChunk("line1\nline2\n")
-		emitter.emitCommandOutputDone()
+		emitter.emitCommandOutputDone(17)
 
 		// This completion say is expected from the extension, but should be suppressed
 		// because we already streamed and completed via commandExecutionStatus.
@@ -339,7 +339,7 @@ describe("JsonEventEmitter streaming deltas", () => {
 			type: "tool_result",
 			id: commandId,
 			subtype: "command",
-			tool_result: { name: "execute_command" },
+			tool_result: { name: "execute_command", exitCode: 17 },
 			done: true,
 		})
 	})

+ 20 - 4
apps/cli/src/agent/json-event-emitter.ts

@@ -310,7 +310,12 @@ export class JsonEventEmitter {
 		return normalized.startsWith(previous) ? normalized.slice(previous.length) : normalized
 	}
 
-	private emitCommandOutputEvent(commandId: number, fullOutput: string | undefined, isDone: boolean): void {
+	private emitCommandOutputEvent(
+		commandId: number,
+		fullOutput: string | undefined,
+		isDone: boolean,
+		exitCode?: number,
+	): void {
 		if (this.mode === "stream-json") {
 			const outputDelta = this.computeCommandOutputDelta(commandId, fullOutput)
 			const event: JsonEvent = {
@@ -324,6 +329,13 @@ export class JsonEventEmitter {
 				event.tool_result = { name: "execute_command", output: outputDelta }
 			}
 
+			if (isDone && exitCode !== undefined) {
+				event.tool_result = {
+					...(event.tool_result ?? { name: "execute_command" }),
+					exitCode,
+				}
+			}
+
 			if (isDone) {
 				event.done = true
 				this.previousCommandOutputByToolUseId.delete(commandId)
@@ -347,7 +359,11 @@ export class JsonEventEmitter {
 			type: "tool_result",
 			id: commandId,
 			subtype: "command",
-			tool_result: { name: "execute_command", output: fullOutput },
+			tool_result: {
+				name: "execute_command",
+				output: fullOutput,
+				...(isDone && exitCode !== undefined ? { exitCode } : {}),
+			},
 			...(isDone ? { done: true } : {}),
 		})
 
@@ -371,7 +387,7 @@ export class JsonEventEmitter {
 		this.emitCommandOutputEvent(commandId, outputSnapshot, false)
 	}
 
-	public emitCommandOutputDone(): void {
+	public emitCommandOutputDone(exitCode?: number): void {
 		const commandId = this.activeCommandToolUseId
 		if (commandId === undefined) {
 			return
@@ -379,7 +395,7 @@ export class JsonEventEmitter {
 
 		this.statusDrivenCommandOutputIds.add(commandId)
 		this.suppressNextCommandOutputSay = true
-		this.emitCommandOutputEvent(commandId, undefined, true)
+		this.emitCommandOutputEvent(commandId, undefined, true, exitCode)
 	}
 
 	/**

+ 5 - 1
apps/cli/src/commands/cli/stdin-stream.ts

@@ -419,7 +419,11 @@ export async function runStdinStreamMode({ host, jsonEmitter, setStreamRequestId
 				parsedStatus.status === "timeout" ||
 				parsedStatus.status === "fallback"
 			) {
-				jsonEmitter.emitCommandOutputDone()
+				const exitCode =
+					parsedStatus.status === "exited" && typeof parsedStatus.exitCode === "number"
+						? parsedStatus.exitCode
+						: undefined
+				jsonEmitter.emitCommandOutputDone(exitCode)
 				return
 			}
 

+ 1 - 0
packages/types/src/cli.ts

@@ -115,6 +115,7 @@ export const rooCliToolResultSchema = z.object({
 	name: z.string(),
 	output: z.string().optional(),
 	error: z.string().optional(),
+	exitCode: z.number().optional(),
 })
 
 export type RooCliToolResult = z.infer<typeof rooCliToolResultSchema>