From 1be025bb72b8882f7c1212a5e7d0e07a0b1ee81e Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 14 Jul 2026 20:20:46 +0000 Subject: [PATCH] fix(cursor): propagated returned tool error status Cursor exec bridges derived failure state only from thrown exceptions, so structured AgentToolResult.isError failures were emitted as successes. Propagate the returned flag through standard and streaming shell execution, with regression coverage for both paths. --- packages/coding-agent/src/cursor.ts | 2 + .../coding-agent/test/cursor-exec.test.ts | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/packages/coding-agent/src/cursor.ts b/packages/coding-agent/src/cursor.ts index 5865189c5..4bc29fb0c 100644 --- a/packages/coding-agent/src/cursor.ts +++ b/packages/coding-agent/src/cursor.ts @@ -93,6 +93,7 @@ async function executeTool( result = buildToolErrorResult(message); isError = true; } + isError ||= result.isError === true; const sanitizedFinalResult: AgentToolResult = { content: result.content.map(c => (c.type === "text" ? { ...c, text: sanitizeText(c.text) } : c)), @@ -283,6 +284,7 @@ export class CursorExecHandlers implements ICursorExecHandlers { result = buildToolErrorResult(message); isError = true; } + isError ||= result.isError === true; // onUpdate may not fire for every chunk — flush any remaining output // from the final result that wasn't already streamed. diff --git a/packages/coding-agent/test/cursor-exec.test.ts b/packages/coding-agent/test/cursor-exec.test.ts index fde7857f4..9dc1c49bb 100644 --- a/packages/coding-agent/test/cursor-exec.test.ts +++ b/packages/coding-agent/test/cursor-exec.test.ts @@ -2,10 +2,14 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; +import { create } from "@bufbuild/protobuf"; +import type { AgentEvent, AgentTool } from "@oh-my-pi/pi-agent-core"; +import { ReadArgsSchema, ShellArgsSchema } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { CursorExecHandlers } from "@oh-my-pi/pi-coding-agent/cursor"; import { GrepTool, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; import { removeWithRetries } from "@oh-my-pi/pi-utils"; +import { type } from "arktype"; function createTestSession(cwd: string, overrides: Partial = {}): ToolSession { return { @@ -65,3 +69,55 @@ describe("CursorExecHandlers.grep bridge", () => { expect((sensitiveResult.details as { matchCount?: number } | undefined)?.matchCount).toBe(1); }); }); + +describe("CursorExecHandlers error results", () => { + const rewrittenErrorTool = (name: string): AgentTool => ({ + name, + label: name, + description: "returns a rewritten tool failure", + parameters: type({}), + execute: async () => ({ + content: [{ type: "text", text: "Enriched recovery guidance" }], + details: { enriched: true }, + isError: true, + }), + }); + + it("propagates returned isError through the standard exec bridge", async () => { + const events: AgentEvent[] = []; + const handlers = new CursorExecHandlers({ + cwd: ".", + tools: new Map([["read", rewrittenErrorTool("read")]]), + emitEvent: event => events.push(event), + }); + + const result = await handlers.read(create(ReadArgsSchema, { toolCallId: "call-read", path: "ignored" })); + expect(result.isError).toBe(true); + expect(result.content).toEqual([{ type: "text", text: "Enriched recovery guidance" }]); + const end = events.find(event => event.type === "tool_execution_end"); + expect(end?.isError).toBe(true); + }); + + it("propagates returned isError through the shell stream bridge", async () => { + const events: AgentEvent[] = []; + const stdout: string[] = []; + const handlers = new CursorExecHandlers({ + cwd: ".", + tools: new Map([["bash", rewrittenErrorTool("bash")]]), + emitEvent: event => events.push(event), + }); + + const result = await handlers.shellStream( + create(ShellArgsSchema, { toolCallId: "call-shell", command: "ignored" }), + { + onStdout: data => stdout.push(data), + onStderr: () => {}, + }, + ); + expect(result.isError).toBe(true); + expect(result.content).toEqual([{ type: "text", text: "Enriched recovery guidance" }]); + expect(stdout).toEqual(["Enriched recovery guidance"]); + const end = events.find(event => event.type === "tool_execution_end"); + expect(end?.isError).toBe(true); + }); +});