Skip to content

Commit 4e8cbdd

Browse files
committed
BOX-137: fixes
1 parent 7cad260 commit 4e8cbdd

3 files changed

Lines changed: 98 additions & 11 deletions

File tree

packages/sdk/src/__tests__/box-agent-run.test.ts

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ describe("box.agent.run", () => {
2929

3030
it("calls onToolUse callback", async () => {
3131
const { box, fetchMock } = await createTestBox();
32-
const tools: Array<{ name: string; input: Record<string, unknown> }> = [];
32+
const tools: Array<{ toolCallId: string; name: string; input: Record<string, unknown> }> = [];
3333

3434
fetchMock.mockResolvedValueOnce(
3535
mockSSEResponse([
3636
{ event: "run_start", data: { run_id: "r1" } },
37-
{ event: "tool", data: { name: "Read", input: { path: "/test" } } },
37+
{ event: "tool", data: { id: "tool-1", name: "Read", input: { path: "/test" } } },
3838
{ event: "done", data: {} },
3939
]),
4040
);
@@ -45,9 +45,30 @@ describe("box.agent.run", () => {
4545
});
4646

4747
expect(tools).toHaveLength(1);
48+
expect(tools[0]!.toolCallId).toBe("tool-1");
4849
expect(tools[0]!.name).toBe("Read");
4950
});
5051

52+
it("calls onToolResult callback", async () => {
53+
const { box, fetchMock } = await createTestBox();
54+
const results: Array<{ toolCallId: string; output: unknown }> = [];
55+
56+
fetchMock.mockResolvedValueOnce(
57+
mockSSEResponse([
58+
{ event: "run_start", data: { run_id: "r1" } },
59+
{ event: "tool_result", data: { toolCallId: "tool-1", output: { ok: true } } },
60+
{ event: "done", data: {} },
61+
]),
62+
);
63+
64+
await box.agent.run({
65+
prompt: "test",
66+
onToolResult: (result) => results.push(result),
67+
});
68+
69+
expect(results).toEqual([{ toolCallId: "tool-1", output: { ok: true } }]);
70+
});
71+
5172
it("parses structured output with responseSchema", async () => {
5273
const { box, fetchMock } = await createTestBox();
5374

@@ -437,12 +458,12 @@ describe("box.agent.stream", () => {
437458

438459
it("yields tool-call chunks and calls onToolUse", async () => {
439460
const { box, fetchMock } = await createTestBox();
440-
const tools: Array<{ name: string; input: Record<string, unknown> }> = [];
461+
const tools: Array<{ toolCallId: string; name: string; input: Record<string, unknown> }> = [];
441462

442463
fetchMock.mockResolvedValueOnce(
443464
mockSSEResponse([
444465
{ event: "run_start", data: { run_id: "r1" } },
445-
{ event: "tool", data: { name: "Write", input: { path: "/x" } } },
466+
{ event: "tool", data: { id: "tool-2", name: "Write", input: { path: "/x" } } },
446467
{ event: "text", data: { text: "done" } },
447468
{ event: "done", data: {} },
448469
]),
@@ -458,15 +479,51 @@ describe("box.agent.stream", () => {
458479
}
459480

460481
expect(tools).toHaveLength(1);
482+
expect(tools[0]!.toolCallId).toBe("tool-2");
461483
expect(tools[0]!.name).toBe("Write");
462484
const toolChunks = chunks.filter((c) => c.type === "tool-call");
463485
expect(toolChunks).toHaveLength(1);
486+
expect(toolChunks[0]).toEqual({
487+
type: "tool-call",
488+
toolCallId: "tool-2",
489+
toolName: "Write",
490+
input: { path: "/x" },
491+
});
464492
const textChunks = chunks.filter(
465493
(c): c is Extract<Chunk, { type: "text-delta" }> => c.type === "text-delta",
466494
);
467495
expect(textChunks.map((c) => c.text)).toEqual(["done"]);
468496
});
469497

498+
it("yields tool-result chunks and calls onToolResult", async () => {
499+
const { box, fetchMock } = await createTestBox();
500+
const results: Array<{ toolCallId: string; output: unknown }> = [];
501+
502+
fetchMock.mockResolvedValueOnce(
503+
mockSSEResponse([
504+
{ event: "run_start", data: { run_id: "r1" } },
505+
{ event: "tool_result", data: { tool_use_id: "tool-3", output: "ok" } },
506+
{ event: "done", data: {} },
507+
]),
508+
);
509+
510+
const run = await box.agent.stream({
511+
prompt: "test",
512+
onToolResult: (result) => results.push(result),
513+
});
514+
const chunks: Chunk[] = [];
515+
for await (const chunk of run) {
516+
chunks.push(chunk);
517+
}
518+
519+
expect(results).toEqual([{ toolCallId: "tool-3", output: "ok" }]);
520+
expect(chunks).toContainEqual({
521+
type: "tool-result",
522+
toolCallId: "tool-3",
523+
output: "ok",
524+
});
525+
});
526+
470527
it("yields all chunk types in order", async () => {
471528
const { box, fetchMock } = await createTestBox();
472529

@@ -475,7 +532,8 @@ describe("box.agent.stream", () => {
475532
{ event: "run_start", data: { run_id: "r1" } },
476533
{ event: "text", data: { text: "Hello " } },
477534
{ event: "thinking", data: { text: "trace" } },
478-
{ event: "tool", data: { name: "Write", input: { path: "/x" } } },
535+
{ event: "tool", data: { toolCallId: "tool-4", name: "Write", input: { path: "/x" } } },
536+
{ event: "tool_result", data: { tool_use_id: "tool-4", output: "done" } },
479537
{
480538
event: "done",
481539
data: { output: "Hello world", input_tokens: 7, output_tokens: 9, session_id: "s1" },
@@ -495,6 +553,7 @@ describe("box.agent.stream", () => {
495553
"text-delta",
496554
"reasoning",
497555
"tool-call",
556+
"tool-result",
498557
"finish",
499558
"stats",
500559
]);

packages/sdk/src/client.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -943,7 +943,22 @@ export class Box<TProvider = unknown> {
943943
break;
944944
}
945945
case "tool": {
946-
options.onToolUse?.({ name: parsed.name, input: parsed.input });
946+
const toolCallId =
947+
parsed.tool_call_id ?? parsed.id ?? parsed.tool_use_id ?? parsed.toolCallId ?? "";
948+
options.onToolUse?.({
949+
toolCallId,
950+
name: parsed.name ?? "",
951+
input: parsed.input ?? {},
952+
});
953+
break;
954+
}
955+
case "tool_result": {
956+
const toolCallId =
957+
parsed.tool_call_id ?? parsed.id ?? parsed.tool_use_id ?? parsed.toolCallId ?? "";
958+
options.onToolResult?.({
959+
toolCallId,
960+
output: parsed.output,
961+
});
947962
break;
948963
}
949964
case "done": {
@@ -1103,16 +1118,25 @@ export class Box<TProvider = unknown> {
11031118
toolName: parsed.name ?? "",
11041119
input: parsed.input ?? {},
11051120
};
1106-
options.onToolUse?.({ name: parsed.name ?? "", input: parsed.input ?? {} });
1121+
options.onToolUse?.({
1122+
toolCallId,
1123+
name: parsed.name ?? "",
1124+
input: parsed.input ?? {},
1125+
});
11071126
return chunk;
11081127
}
11091128
case "tool_result": {
1129+
const toolCallId =
1130+
parsed.tool_call_id ?? parsed.id ?? parsed.tool_use_id ?? parsed.toolCallId ?? "";
11101131
const chunk: Chunk = {
11111132
type: "tool-result",
1112-
toolCallId:
1113-
parsed.tool_call_id ?? parsed.id ?? parsed.tool_use_id ?? parsed.toolCallId ?? "",
1133+
toolCallId,
11141134
output: parsed.output,
11151135
};
1136+
options.onToolResult?.({
1137+
toolCallId,
1138+
output: parsed.output,
1139+
});
11161140
return chunk;
11171141
}
11181142
case "done": {

packages/sdk/src/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,9 @@ export interface StreamOptions<TProvider = unknown> {
475475
/** Timeout in milliseconds — aborts if exceeded */
476476
timeout?: number;
477477
/** Tool use callback — called when the agent invokes a tool (Read, Write, Bash, etc.) */
478-
onToolUse?: (tool: { name: string; input: Record<string, unknown> }) => void;
478+
onToolUse?: (tool: { toolCallId: string; name: string; input: Record<string, unknown> }) => void;
479+
/** Tool result callback — called when a tool invocation completes */
480+
onToolResult?: (result: { toolCallId: string; output: unknown }) => void;
479481
}
480482

481483
/**
@@ -495,7 +497,9 @@ export interface RunOptions<T = undefined, TProvider = unknown> {
495497
/** Retries with exponential backoff on transient failures */
496498
maxRetries?: number;
497499
/** Tool use callback — called when the agent invokes a tool (Read, Write, Bash, etc.) */
498-
onToolUse?: (tool: { name: string; input: Record<string, unknown> }) => void;
500+
onToolUse?: (tool: { toolCallId: string; name: string; input: Record<string, unknown> }) => void;
501+
/** Tool result callback — called when a tool invocation completes */
502+
onToolResult?: (result: { toolCallId: string; output: unknown }) => void;
499503
/** Webhook — fire-and-forget, POST to URL on completion */
500504
webhook?: WebhookConfig;
501505
}

0 commit comments

Comments
 (0)