Skip to content

Commit 22dd61a

Browse files
authored
fix(tools): throw on bash command failure so framework emits tool error (#100)
1 parent 4100e7e commit 22dd61a

2 files changed

Lines changed: 23 additions & 30 deletions

File tree

packages/core/src/__tests__/runner/sandbox-tools.test.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,16 @@ describe("createSandboxTools", () => {
8888
expect(result.details).toMatchObject({ exitCode: 0 });
8989
});
9090

91-
it("returns structured error with exit code and stderr on non-zero exit", async () => {
91+
it("throws on non-zero exit so the framework reports a tool error", async () => {
9292
const tools = createSandboxTools({ cwd: tmpDir });
9393
const bash = tools.find((t) => t.name === "bash")!;
9494

95-
const result = await bash.execute("call-2", {
96-
command: "echo err >&2; exit 42",
97-
});
98-
99-
expect(result.details.exitCode).toBe(42);
100-
expect((result as any).isError).toBe(true);
101-
expect((result.content[0] as any).text).toContain("err");
95+
// The framework only marks a tool call as `isError: true` when the
96+
// tool throws — returning `{ isError: true }` is ignored by
97+
// providers (notably Gemini). The bash tool must therefore raise.
98+
await expect(
99+
bash.execute("call-2", { command: "echo err >&2; exit 42" }),
100+
).rejects.toThrow(/err|exit code 42/);
102101
});
103102

104103
it("uses custom cwd when provided", async () => {
@@ -113,28 +112,24 @@ describe("createSandboxTools", () => {
113112
});
114113
});
115114

116-
it("returns error result when cwd does not exist", async () => {
115+
it("throws when cwd does not exist", async () => {
117116
const tools = createSandboxTools({
118117
cwd: join(tmpDir, "nonexistent-cwd"),
119118
});
120119
const bash = tools.find((t) => t.name === "bash")!;
121120

122-
const result = await bash.execute("call-4", { command: "echo ok" });
123-
124-
expect((result as any).isError).toBe(true);
125-
expect((result.content[0] as any).text).toContain("ENOENT");
121+
await expect(
122+
bash.execute("call-4", { command: "echo ok" }),
123+
).rejects.toThrow(/ENOENT/);
126124
});
127125

128-
it("returns error result when command times out", async () => {
126+
it("throws when command times out", async () => {
129127
const tools = createSandboxTools({ cwd: tmpDir });
130128
const bash = tools.find((t) => t.name === "bash")!;
131129

132-
const result = await bash.execute("call-5", {
133-
command: "sleep 10",
134-
timeout: 1,
135-
});
136-
137-
expect((result as any).isError).toBe(true);
130+
await expect(
131+
bash.execute("call-5", { command: "sleep 10", timeout: 1 }),
132+
).rejects.toThrow();
138133
});
139134
});
140135

packages/core/src/runner/sandbox-tools.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,15 @@ export function createSandboxTools(options?: {
4040
const exitCode = (err as { status?: number }).status ?? 1;
4141
const stderr = (err as { stderr?: string }).stderr ?? "";
4242
const stdout = (err as { stdout?: string }).stdout ?? "";
43+
const message = (err as { message?: string }).message ?? "";
4344
const output = [stdout, stderr].filter(Boolean).join("\n");
44-
return {
45-
content: [
46-
{
47-
type: "text" as const,
48-
text: output || String(err),
49-
},
50-
],
51-
details: { exitCode },
52-
isError: true,
53-
};
45+
// Throw so pi-agent-core marks the tool call as `isError: true`
46+
// and providers (notably Gemini) emit the error-shaped tool
47+
// response. Returning `{ isError: true }` is ignored by the
48+
// framework — only thrown errors propagate as tool errors.
49+
throw new Error(
50+
output || message || `Command exited with code ${exitCode}`,
51+
);
5452
}
5553
},
5654
};

0 commit comments

Comments
 (0)