From 5c0623be5f3b7b4638f7ef1f708e183fcd4daf3c Mon Sep 17 00:00:00 2001 From: Aditya-thesys Date: Thu, 5 Nov 2026 13:46:03 +0530 Subject: [PATCH 1/9] fix(server): allow 48 tool calls per script run The dashboard interpreter capped a script run at 16 tool calls before the sandbox's own limit applied. Raise it to 48 to match sandbox-runner#13; rounds stay at 8. Tests cover 48 calls (in one round and across rounds), the 49th call, and the 8-round limit. Claude-Session: https://claude.ai/code/session_01DuFZ75cVMd52YVqv9C6XBF --- .changeset/server-tool-call-limit.md | 5 +++ packages/server/src/tools/execute.test.ts | 52 +++++++++++++++++++++++ packages/server/src/tools/execute.ts | 4 +- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 .changeset/server-tool-call-limit.md create mode 100644 packages/server/src/tools/execute.test.ts diff --git a/.changeset/server-tool-call-limit.md b/.changeset/server-tool-call-limit.md new file mode 100644 index 0000000000..6876592a24 --- /dev/null +++ b/.changeset/server-tool-call-limit.md @@ -0,0 +1,5 @@ +--- +"@openuidev/server": patch +--- + +A dashboard script may now make up to 48 tool calls per run (was 16), matching OpenUI Cloud's sandbox. Rounds stay at 8. diff --git a/packages/server/src/tools/execute.test.ts b/packages/server/src/tools/execute.test.ts new file mode 100644 index 0000000000..1d3548ab7c --- /dev/null +++ b/packages/server/src/tools/execute.test.ts @@ -0,0 +1,52 @@ +import { expect, it } from "vitest"; +import type { CloudApi } from "../client/api"; +import { createInterpreter } from "./execute"; +import type { DataTool, ExecutionContext } from "./types"; + +/** OpenUI Cloud running a script that asks for `perRound[i]` tool calls in round i, then finishes. */ +function cloud(perRound: number[]): CloudApi { + let round = 0; + return { + async post() { + const calls = perRound[round++]; + if (calls === undefined) return { status: "done", result: { ok: true } }; + return { + status: "tool_calls", + state: `state-${round}`, + calls: Array.from({ length: calls }, (_, i) => ({ + type: "function_call", + call_id: `call-${round}-${i}`, + name: "sales", + arguments: "{}", + })), + }; + }, + }; +} + +const tools = async () => + ({ sales: { execute: async () => ({ total: 1 }) } }) as unknown as Record< + string, + DataTool + >; + +const run = (perRound: number[]) => + createInterpreter(cloud(perRound), tools).execute( + { name: "totals", id: "art_1" }, + { userId: "user_1" }, + ); + +it("runs a script that makes 48 tool calls", async () => { + await expect(run([16, 16, 16])).resolves.toEqual({ ok: true }); + await expect(run([48])).resolves.toEqual({ ok: true }); +}); + +it("stops a script that asks for a 49th tool call", async () => { + await expect(run([48, 1])).rejects.toMatchObject({ code: "script_limit" }); + await expect(run([49])).rejects.toMatchObject({ code: "script_limit" }); +}); + +it("allows 8 rounds of tool calls, not 9", async () => { + await expect(run(Array(8).fill(1))).resolves.toEqual({ ok: true }); + await expect(run(Array(9).fill(1))).rejects.toMatchObject({ code: "script_limit" }); +}); diff --git a/packages/server/src/tools/execute.ts b/packages/server/src/tools/execute.ts index 4968150f7c..4d7426eac3 100644 --- a/packages/server/src/tools/execute.ts +++ b/packages/server/src/tools/execute.ts @@ -17,8 +17,10 @@ import { type ToolHandler, } from "./types"; +// The sandbox's limits per script run (sandbox-runner MAX_TOOL_CALLS / max rounds): +// tool calls in total across rounds, and rounds of calls the script waits on. const MAX_ROUNDS = 8; -const MAX_TOOL_CALLS = 16; +const MAX_TOOL_CALLS = 48; const DEFAULT_TIMEOUT_MS = 60_000; function invalidResponse(): never { From 0604345e470f49ecd513ef5ab6fa0cb4ff009b12 Mon Sep 17 00:00:00 2001 From: Aditya-thesys Date: Thu, 5 Nov 2026 14:30:55 +0530 Subject: [PATCH 2/9] chore(server): drop the interpreter limit test Claude-Session: https://claude.ai/code/session_01DuFZ75cVMd52YVqv9C6XBF --- packages/server/src/tools/execute.test.ts | 52 ----------------------- 1 file changed, 52 deletions(-) delete mode 100644 packages/server/src/tools/execute.test.ts diff --git a/packages/server/src/tools/execute.test.ts b/packages/server/src/tools/execute.test.ts deleted file mode 100644 index 1d3548ab7c..0000000000 --- a/packages/server/src/tools/execute.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { expect, it } from "vitest"; -import type { CloudApi } from "../client/api"; -import { createInterpreter } from "./execute"; -import type { DataTool, ExecutionContext } from "./types"; - -/** OpenUI Cloud running a script that asks for `perRound[i]` tool calls in round i, then finishes. */ -function cloud(perRound: number[]): CloudApi { - let round = 0; - return { - async post() { - const calls = perRound[round++]; - if (calls === undefined) return { status: "done", result: { ok: true } }; - return { - status: "tool_calls", - state: `state-${round}`, - calls: Array.from({ length: calls }, (_, i) => ({ - type: "function_call", - call_id: `call-${round}-${i}`, - name: "sales", - arguments: "{}", - })), - }; - }, - }; -} - -const tools = async () => - ({ sales: { execute: async () => ({ total: 1 }) } }) as unknown as Record< - string, - DataTool - >; - -const run = (perRound: number[]) => - createInterpreter(cloud(perRound), tools).execute( - { name: "totals", id: "art_1" }, - { userId: "user_1" }, - ); - -it("runs a script that makes 48 tool calls", async () => { - await expect(run([16, 16, 16])).resolves.toEqual({ ok: true }); - await expect(run([48])).resolves.toEqual({ ok: true }); -}); - -it("stops a script that asks for a 49th tool call", async () => { - await expect(run([48, 1])).rejects.toMatchObject({ code: "script_limit" }); - await expect(run([49])).rejects.toMatchObject({ code: "script_limit" }); -}); - -it("allows 8 rounds of tool calls, not 9", async () => { - await expect(run(Array(8).fill(1))).resolves.toEqual({ ok: true }); - await expect(run(Array(9).fill(1))).rejects.toMatchObject({ code: "script_limit" }); -}); From ad55de0ef54da9402f29058500ed99f5059f99f8 Mon Sep 17 00:00:00 2001 From: Aditya-thesys Date: Sun, 8 Nov 2026 16:02:02 +0530 Subject: [PATCH 3/9] fix(server): leave the tool-call limit to the sandbox, keep a 256 backstop OpenUI Cloud's sandbox enforces the tool-call limit per script run (48) and fails the run itself. Hardcoding the same number here meant every limit change needed a release of this package and every app to upgrade. Keep only a high backstop against a misbehaving server. Rounds stay at 8 (only this package counts them). --- .changeset/server-tool-call-limit.md | 2 +- packages/server/src/tools/execute.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.changeset/server-tool-call-limit.md b/.changeset/server-tool-call-limit.md index 6876592a24..47b6dabbb8 100644 --- a/.changeset/server-tool-call-limit.md +++ b/.changeset/server-tool-call-limit.md @@ -2,4 +2,4 @@ "@openuidev/server": patch --- -A dashboard script may now make up to 48 tool calls per run (was 16), matching OpenUI Cloud's sandbox. Rounds stay at 8. +A dashboard script's tool calls are limited by OpenUI Cloud's sandbox (48 per run), no longer by this package (was 16). The package keeps only a high backstop (256), so a later change to the sandbox's limit needs no upgrade. Rounds stay at 8. diff --git a/packages/server/src/tools/execute.ts b/packages/server/src/tools/execute.ts index 4d7426eac3..714d4c02e8 100644 --- a/packages/server/src/tools/execute.ts +++ b/packages/server/src/tools/execute.ts @@ -17,10 +17,12 @@ import { type ToolHandler, } from "./types"; -// The sandbox's limits per script run (sandbox-runner MAX_TOOL_CALLS / max rounds): -// tool calls in total across rounds, and rounds of calls the script waits on. +// Rounds of calls a script run may wait on. Only this package counts them. const MAX_ROUNDS = 8; -const MAX_TOOL_CALLS = 48; +// A backstop only: OpenUI Cloud's sandbox enforces the real tool-call limit per +// script run (48) and fails the run itself, so raising it needs no new release of +// this package. This stops a runaway loop if the server misbehaves. +const MAX_TOOL_CALLS = 256; const DEFAULT_TIMEOUT_MS = 60_000; function invalidResponse(): never { From 2134e534c5e3b5c75513ae47fa15f9bf9e2c77c4 Mon Sep 17 00:00:00 2001 From: Aditya-thesys Date: Sun, 8 Nov 2026 22:52:47 +0530 Subject: [PATCH 4/9] docs(server): no hardcoded sandbox limit in the comment; note the error code and round size in the changeset --- .changeset/server-tool-call-limit.md | 2 +- packages/server/src/tools/execute.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/server-tool-call-limit.md b/.changeset/server-tool-call-limit.md index 47b6dabbb8..4659316c3b 100644 --- a/.changeset/server-tool-call-limit.md +++ b/.changeset/server-tool-call-limit.md @@ -2,4 +2,4 @@ "@openuidev/server": patch --- -A dashboard script's tool calls are limited by OpenUI Cloud's sandbox (48 per run), no longer by this package (was 16). The package keeps only a high backstop (256), so a later change to the sandbox's limit needs no upgrade. Rounds stay at 8. +A dashboard script's tool calls are limited by OpenUI Cloud's sandbox (48 per run), no longer by this package (was 16). The package keeps only a high backstop (256), so a later change to the sandbox's limit needs no upgrade. Rounds stay at 8. A run over the sandbox's limit fails with code `SCRIPT_ERROR` (message "TOO_MANY_TOOL_CALLS: …") instead of `script_limit`. A round can now run up to 48 of your tools at once. diff --git a/packages/server/src/tools/execute.ts b/packages/server/src/tools/execute.ts index 714d4c02e8..aa79e3ad0d 100644 --- a/packages/server/src/tools/execute.ts +++ b/packages/server/src/tools/execute.ts @@ -20,7 +20,7 @@ import { // Rounds of calls a script run may wait on. Only this package counts them. const MAX_ROUNDS = 8; // A backstop only: OpenUI Cloud's sandbox enforces the real tool-call limit per -// script run (48) and fails the run itself, so raising it needs no new release of +// script run and fails the run itself, so changing it needs no new release of // this package. This stops a runaway loop if the server misbehaves. const MAX_TOOL_CALLS = 256; const DEFAULT_TIMEOUT_MS = 60_000; From 6a54bed26111cd075d381216490d8a8139ef2922 Mon Sep 17 00:00:00 2001 From: Aditya-thesys Date: Mon, 9 Nov 2026 17:09:25 +0530 Subject: [PATCH 5/9] fix(server): run a round's tools at most 16 at a time A round's whole batch ran at once with Promise.all, so one large batch (48 from an honest sandbox, up to the 256 backstop from a misbehaving server) started that many of the developer's tools simultaneously. A small pool caps it at 16, keeps the result order, and starts no more tools once the run is aborted or times out. --- .changeset/server-tool-call-limit.md | 2 +- packages/server/src/tools/execute.ts | 31 +++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/.changeset/server-tool-call-limit.md b/.changeset/server-tool-call-limit.md index 4659316c3b..941681b988 100644 --- a/.changeset/server-tool-call-limit.md +++ b/.changeset/server-tool-call-limit.md @@ -2,4 +2,4 @@ "@openuidev/server": patch --- -A dashboard script's tool calls are limited by OpenUI Cloud's sandbox (48 per run), no longer by this package (was 16). The package keeps only a high backstop (256), so a later change to the sandbox's limit needs no upgrade. Rounds stay at 8. A run over the sandbox's limit fails with code `SCRIPT_ERROR` (message "TOO_MANY_TOOL_CALLS: …") instead of `script_limit`. A round can now run up to 48 of your tools at once. +A dashboard script's tool calls are limited by OpenUI Cloud's sandbox (48 per run), no longer by this package (was 16). The package keeps only a high backstop (256), so a later change to the sandbox's limit needs no upgrade. Rounds stay at 8. A run over the sandbox's limit fails with code `SCRIPT_ERROR` (message "TOO_MANY_TOOL_CALLS: …") instead of `script_limit`. A round can now run up to 48 of your tools at once. Your tools from one round now run at most 16 at a time (the batch was run all at once before). diff --git a/packages/server/src/tools/execute.ts b/packages/server/src/tools/execute.ts index aa79e3ad0d..69cb905456 100644 --- a/packages/server/src/tools/execute.ts +++ b/packages/server/src/tools/execute.ts @@ -23,8 +23,29 @@ const MAX_ROUNDS = 8; // script run and fails the run itself, so changing it needs no new release of // this package. This stops a runaway loop if the server misbehaves. const MAX_TOOL_CALLS = 256; +// Your tools from one round run at most this many at a time, so a large batch +// can't flood your backend; the results still go back together. +const MAX_CONCURRENT_TOOLS = 16; const DEFAULT_TIMEOUT_MS = 60_000; +/** `Promise.all(items.map(fn))` with at most `limit` running at once; order kept. */ +async function mapWithLimit( + items: T[], + limit: number, + fn: (item: T) => Promise, +): Promise { + const results = new Array(items.length); + let next = 0; + const worker = async () => { + while (next < items.length) { + const index = next++; + results[index] = await fn(items[index]!); + } + }; + await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker)); + return results; +} + function invalidResponse(): never { throw new ToolExecutionError("Invalid script execution response", "invalid_response", 502); } @@ -185,8 +206,12 @@ export function createInterpreter => { + toolResults = await mapWithLimit( + calls as ScriptToolCall[], + MAX_CONCURRENT_TOOLS, + async (call): Promise => { + // A timed-out or aborted run starts no more tools. + signal.throwIfAborted(); try { const toolArgs: unknown = JSON.parse(call.arguments); if (!isRecord(toolArgs)) @@ -214,7 +239,7 @@ export function createInterpreter Date: Mon, 9 Nov 2026 17:09:37 +0530 Subject: [PATCH 6/9] docs(server): changeset wording for the per-round pool --- .changeset/server-tool-call-limit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/server-tool-call-limit.md b/.changeset/server-tool-call-limit.md index 941681b988..d3f3d14754 100644 --- a/.changeset/server-tool-call-limit.md +++ b/.changeset/server-tool-call-limit.md @@ -2,4 +2,4 @@ "@openuidev/server": patch --- -A dashboard script's tool calls are limited by OpenUI Cloud's sandbox (48 per run), no longer by this package (was 16). The package keeps only a high backstop (256), so a later change to the sandbox's limit needs no upgrade. Rounds stay at 8. A run over the sandbox's limit fails with code `SCRIPT_ERROR` (message "TOO_MANY_TOOL_CALLS: …") instead of `script_limit`. A round can now run up to 48 of your tools at once. Your tools from one round now run at most 16 at a time (the batch was run all at once before). +A dashboard script's tool calls are limited by OpenUI Cloud's sandbox (48 per run), no longer by this package (was 16). The package keeps only a high backstop (256), so a later change to the sandbox's limit needs no upgrade. Rounds stay at 8. A run over the sandbox's limit fails with code `SCRIPT_ERROR` (message "TOO_MANY_TOOL_CALLS: …") instead of `script_limit`. A round can now ask for up to 48 of your tools; they run at most 16 at a time (a round's batch used to run all at once). From 7b090b7e49eb56adc08c1a6557d84d1456faf3ff Mon Sep 17 00:00:00 2001 From: Aditya-thesys Date: Mon, 9 Nov 2026 17:19:12 +0530 Subject: [PATCH 7/9] chore(server): move the per-round tool pool to its own PR The pool (at most 16 tools at a time per round) goes in a separate PR so this one stays the call-limit change only. --- .changeset/server-tool-call-limit.md | 2 +- packages/server/src/tools/execute.ts | 31 +++------------------------- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/.changeset/server-tool-call-limit.md b/.changeset/server-tool-call-limit.md index d3f3d14754..4659316c3b 100644 --- a/.changeset/server-tool-call-limit.md +++ b/.changeset/server-tool-call-limit.md @@ -2,4 +2,4 @@ "@openuidev/server": patch --- -A dashboard script's tool calls are limited by OpenUI Cloud's sandbox (48 per run), no longer by this package (was 16). The package keeps only a high backstop (256), so a later change to the sandbox's limit needs no upgrade. Rounds stay at 8. A run over the sandbox's limit fails with code `SCRIPT_ERROR` (message "TOO_MANY_TOOL_CALLS: …") instead of `script_limit`. A round can now ask for up to 48 of your tools; they run at most 16 at a time (a round's batch used to run all at once). +A dashboard script's tool calls are limited by OpenUI Cloud's sandbox (48 per run), no longer by this package (was 16). The package keeps only a high backstop (256), so a later change to the sandbox's limit needs no upgrade. Rounds stay at 8. A run over the sandbox's limit fails with code `SCRIPT_ERROR` (message "TOO_MANY_TOOL_CALLS: …") instead of `script_limit`. A round can now run up to 48 of your tools at once. diff --git a/packages/server/src/tools/execute.ts b/packages/server/src/tools/execute.ts index 69cb905456..aa79e3ad0d 100644 --- a/packages/server/src/tools/execute.ts +++ b/packages/server/src/tools/execute.ts @@ -23,29 +23,8 @@ const MAX_ROUNDS = 8; // script run and fails the run itself, so changing it needs no new release of // this package. This stops a runaway loop if the server misbehaves. const MAX_TOOL_CALLS = 256; -// Your tools from one round run at most this many at a time, so a large batch -// can't flood your backend; the results still go back together. -const MAX_CONCURRENT_TOOLS = 16; const DEFAULT_TIMEOUT_MS = 60_000; -/** `Promise.all(items.map(fn))` with at most `limit` running at once; order kept. */ -async function mapWithLimit( - items: T[], - limit: number, - fn: (item: T) => Promise, -): Promise { - const results = new Array(items.length); - let next = 0; - const worker = async () => { - while (next < items.length) { - const index = next++; - results[index] = await fn(items[index]!); - } - }; - await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker)); - return results; -} - function invalidResponse(): never { throw new ToolExecutionError("Invalid script execution response", "invalid_response", 502); } @@ -206,12 +185,8 @@ export function createInterpreter => { - // A timed-out or aborted run starts no more tools. - signal.throwIfAborted(); + toolResults = await Promise.all( + (calls as ScriptToolCall[]).map(async (call): Promise => { try { const toolArgs: unknown = JSON.parse(call.arguments); if (!isRecord(toolArgs)) @@ -239,7 +214,7 @@ export function createInterpreter Date: Mon, 9 Nov 2026 22:07:58 +0530 Subject: [PATCH 8/9] chore(server): drop the tool-call backstop comment --- packages/server/src/tools/execute.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/server/src/tools/execute.ts b/packages/server/src/tools/execute.ts index aa79e3ad0d..d55c0c4ce2 100644 --- a/packages/server/src/tools/execute.ts +++ b/packages/server/src/tools/execute.ts @@ -19,9 +19,6 @@ import { // Rounds of calls a script run may wait on. Only this package counts them. const MAX_ROUNDS = 8; -// A backstop only: OpenUI Cloud's sandbox enforces the real tool-call limit per -// script run and fails the run itself, so changing it needs no new release of -// this package. This stops a runaway loop if the server misbehaves. const MAX_TOOL_CALLS = 256; const DEFAULT_TIMEOUT_MS = 60_000; From 809e1698edcb066934edcbd0861ec3346ae4f43d Mon Sep 17 00:00:00 2001 From: Aditya-thesys Date: Mon, 9 Nov 2026 22:09:22 +0530 Subject: [PATCH 9/9] chore(server): shorter changeset --- .changeset/server-tool-call-limit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/server-tool-call-limit.md b/.changeset/server-tool-call-limit.md index 4659316c3b..3bef123c01 100644 --- a/.changeset/server-tool-call-limit.md +++ b/.changeset/server-tool-call-limit.md @@ -2,4 +2,4 @@ "@openuidev/server": patch --- -A dashboard script's tool calls are limited by OpenUI Cloud's sandbox (48 per run), no longer by this package (was 16). The package keeps only a high backstop (256), so a later change to the sandbox's limit needs no upgrade. Rounds stay at 8. A run over the sandbox's limit fails with code `SCRIPT_ERROR` (message "TOO_MANY_TOOL_CALLS: …") instead of `script_limit`. A round can now run up to 48 of your tools at once. +Dashboard scripts can make more tool calls per run.