Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/server-tool-pool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openuidev/server": patch
---

Your tools from one script round now run at most 16 at a time, in order, instead of the whole batch at once, and no new tool starts after the run times out or is aborted.
31 changes: 28 additions & 3 deletions packages/server/src/tools/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,27 @@ import {
const MAX_ROUNDS = 8;
const MAX_TOOL_CALLS = 16;
const DEFAULT_TIMEOUT_MS = 60_000;
// 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;

/** `Promise.all(items.map(fn))` with at most `limit` running at once; order kept. */
async function mapWithLimit<T, R>(
items: T[],
limit: number,
fn: (item: T) => Promise<R>,
): Promise<R[]> {
const results = new Array<R>(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);
Expand Down Expand Up @@ -181,8 +202,12 @@ export function createInterpreter<Context extends ExecutionContext = ExecutionCo
seen.add(call["call_id"]);
}
state = response["state"];
toolResults = await Promise.all(
(calls as ScriptToolCall[]).map(async (call): Promise<ScriptToolResult> => {
toolResults = await mapWithLimit(
calls as ScriptToolCall[],
MAX_CONCURRENT_TOOLS,
async (call): Promise<ScriptToolResult> => {
// A timed-out or aborted run starts no more tools.
signal.throwIfAborted();
try {
const toolArgs: unknown = JSON.parse(call.arguments);
if (!isRecord(toolArgs))
Expand Down Expand Up @@ -210,7 +235,7 @@ export function createInterpreter<Context extends ExecutionContext = ExecutionCo
},
};
}
}),
},
);
}
throw new ToolExecutionError("Script execution limit exceeded", "script_limit", 502);
Expand Down
Loading