From ed7240f2c057fc3fed10b2d33f27e38945b9a5a1 Mon Sep 17 00:00:00 2001 From: Sriket Komali Date: Sat, 2 May 2026 21:48:50 -0400 Subject: [PATCH 1/2] fix(ai-isolate-cloudflare): accumulate toolResults across rounds The Cloudflare isolate driver wiped toolResults between need_tools rounds. wrap-code uses sequential tc_ ids that are re-derived from scratch every time the Worker re-executes user code, so prior-round results must remain in the cache. With the wipe, multi-tool programs (e.g. `await A(); await B();`) ping-pong between {tc_0} and {tc_1} and exhaust maxToolRounds, surfacing as MaxRoundsExceeded. Single-tool code worked because only one cache entry was ever needed in a given round. Existing tests covered single-round flows only and used ad-hoc tool-call ids rather than wrap-code's real tc_ shape, so the regression slipped through. Adds a tc_-shaped regression test covering two sequential tool calls. The test fails on the prior implementation and passes after the one-line fix. --- .changeset/accumulate-tool-results.md | 11 ++++ .../src/isolate-driver.ts | 8 ++- .../tests/isolate-driver.test.ts | 64 +++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 .changeset/accumulate-tool-results.md diff --git a/.changeset/accumulate-tool-results.md b/.changeset/accumulate-tool-results.md new file mode 100644 index 000000000..ce2dc3518 --- /dev/null +++ b/.changeset/accumulate-tool-results.md @@ -0,0 +1,11 @@ +--- +"@tanstack/ai-isolate-cloudflare": patch +--- + +fix(ai-isolate-cloudflare): accumulate `toolResults` across rounds in the driver round-trip + +The Cloudflare isolate driver was wiping `toolResults` between rounds. `wrap-code` uses sequential `tc_` ids that are re-derived every round when the Worker re-executes user code, so prior-round results must remain in the cache. With the wipe, multi-tool programs (e.g. `await A(); await B();`) would ping-pong between `{tc_0}` and `{tc_1}` and exhaust `maxToolRounds`, surfacing as `MaxRoundsExceeded`. + +Single-tool code worked because only one cache entry was ever needed in a given round. Existing tests covered single-round flows only and did not exercise real `wrap-code` ids end-to-end, so the regression slipped through. + +Added a `tc_`-shaped regression test that fails on the prior implementation and passes with the merge. diff --git a/packages/typescript/ai-isolate-cloudflare/src/isolate-driver.ts b/packages/typescript/ai-isolate-cloudflare/src/isolate-driver.ts index 9cf596db5..69246098c 100644 --- a/packages/typescript/ai-isolate-cloudflare/src/isolate-driver.ts +++ b/packages/typescript/ai-isolate-cloudflare/src/isolate-driver.ts @@ -174,8 +174,12 @@ class CloudflareIsolateContext implements IsolateContext { // Collect logs from this round allLogs = [...allLogs, ...result.logs] - // Execute tool calls locally - toolResults = {} + // Execute tool calls locally. Accumulate across rounds so prior-round + // results stay cached when the Worker re-executes user code. + // wrap-code uses sequential `tc_` ids re-derived every round; if + // we wipe the cache, multi-tool programs ping-pong between missing + // ids and exhaust `maxToolRounds` (MaxRoundsExceeded). + toolResults = { ...(toolResults ?? {}) } for (const toolCall of result.toolCalls) { const binding = this.bindings[toolCall.name] as diff --git a/packages/typescript/ai-isolate-cloudflare/tests/isolate-driver.test.ts b/packages/typescript/ai-isolate-cloudflare/tests/isolate-driver.test.ts index 42e2dc2d5..6e5a7694f 100644 --- a/packages/typescript/ai-isolate-cloudflare/tests/isolate-driver.test.ts +++ b/packages/typescript/ai-isolate-cloudflare/tests/isolate-driver.test.ts @@ -233,6 +233,70 @@ describe('createCloudflareIsolateDriver', () => { expect(body2.toolResults!['add_1']).toEqual({ success: true, value: 5 }) }) + it('accumulates toolResults across rounds for sequential tool calls', async () => { + // Reproduces a real bug: when user code calls two tools sequentially + // (e.g. `await A(); await B();`), wrap-code re-runs from the start each + // round and re-derives sequential `tc_` ids. If the driver wipes + // toolResults between rounds, round 3 misses tc_0, the wrapper re-throws, + // and the loop ping-pongs between {tc_0} and {tc_1} until MaxRoundsExceeded. + const a = makeBinding('A', async () => 'a') + const b = makeBinding('B', async () => 'b') + + // Round 1: code re-runs, throws on tc_0 + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => + ({ + status: 'need_tools', + toolCalls: [{ id: 'tc_0', name: 'A', args: {} }], + logs: [], + continuationId: 'c1', + }) as ExecuteResponse, + }) + + // Round 2: tc_0 cached, code re-runs, throws on tc_1 + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => + ({ + status: 'need_tools', + toolCalls: [{ id: 'tc_1', name: 'B', args: {} }], + logs: [], + continuationId: 'c2', + }) as ExecuteResponse, + }) + + // Round 3: tc_0 + tc_1 BOTH must be present, code completes + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => + ({ + status: 'done', + success: true, + value: 'ab', + logs: [], + }) as ExecuteResponse, + }) + + const driver = createCloudflareIsolateDriver({ workerUrl: WORKER_URL }) + const context = await driver.createContext({ + bindings: { A: a, B: b }, + }) + + const result = await context.execute( + 'const x = await A({}); const y = await B({}); return x + y', + ) + + expect(result.success).toBe(true) + expect(result.value).toBe('ab') + expect(fetchMock).toHaveBeenCalledTimes(3) + + // Round 3 body MUST include both tc_0 and tc_1 (regression guard) + const body3: ExecuteRequest = JSON.parse(fetchMock.mock.calls[2][1].body) + expect(body3.toolResults!['tc_0']).toEqual({ success: true, value: 'a' }) + expect(body3.toolResults!['tc_1']).toEqual({ success: true, value: 'b' }) + }) + it('handles multiple tool calls in one round', async () => { const getA = makeBinding('getA', async () => 'A') const getB = makeBinding('getB', async () => 'B') From 5fd3edc793b6f9443093366fedace380e7d2b43b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 08:15:39 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- .changeset/accumulate-tool-results.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/accumulate-tool-results.md b/.changeset/accumulate-tool-results.md index ce2dc3518..c368ef7ed 100644 --- a/.changeset/accumulate-tool-results.md +++ b/.changeset/accumulate-tool-results.md @@ -1,5 +1,5 @@ --- -"@tanstack/ai-isolate-cloudflare": patch +'@tanstack/ai-isolate-cloudflare': patch --- fix(ai-isolate-cloudflare): accumulate `toolResults` across rounds in the driver round-trip