From dcfbcc84f2ff0c4c42514d23f487ad0fa951d105 Mon Sep 17 00:00:00 2001 From: tt-a1i Date: Mon, 24 Aug 2026 00:24:20 +0800 Subject: [PATCH] fix(workflows): expose call capacity in sandbox usage --- extensions/workflows/narrator.test.ts | 32 ++++++++++++++++++++++++++ extensions/workflows/sandbox-child.cjs | 11 +++++++++ 2 files changed, 43 insertions(+) diff --git a/extensions/workflows/narrator.test.ts b/extensions/workflows/narrator.test.ts index a8057f17..786eb6c0 100644 --- a/extensions/workflows/narrator.test.ts +++ b/extensions/workflows/narrator.test.ts @@ -203,6 +203,38 @@ test("usage() advances as agents settle, so a post-await read sees them", async assert.deepEqual(readings, { before: 0, afterOne: 1000, afterTwo: 2000 }); }); +test("usage() exposes the host's resolved call capacity", async () => { + const result = await runSandbox( + ` + const capacity = usage().limits; + try { capacity.callsRemaining = 999; } catch { /* frozen */ } + return { + concurrency: capacity.concurrency, + maxAgentCalls: capacity.maxAgentCalls, + callsUsed: capacity.callsUsed, + callsRemaining: capacity.callsRemaining, + }; + `, + { + usageSnapshot: () => ({ + total: 1_000, + limits: { + concurrency: 8, + maxAgentCalls: 128, + callsUsed: 1, + callsRemaining: 127, + }, + }), + }, + ); + assert.deepEqual(result, { + concurrency: 8, + maxAgentCalls: 128, + callsUsed: 1, + callsRemaining: 127, + }); +}); + test("usage() degrades to a zero reading rather than failing the run", async () => { const result = await runSandbox(`return usage().total;`, { usageSnapshot: () => { diff --git a/extensions/workflows/sandbox-child.cjs b/extensions/workflows/sandbox-child.cjs index f77e5334..30124c66 100644 --- a/extensions/workflows/sandbox-child.cjs +++ b/extensions/workflows/sandbox-child.cjs @@ -207,6 +207,11 @@ const BOOTSTRAP = String.raw` const value = raw && typeof raw === "object" ? raw[key] : undefined; return typeof value === "number" && Number.isFinite(value) ? value : 0; }; + const readLimit = (key) => { + const limits = raw && typeof raw === "object" ? raw.limits : undefined; + const value = limits && typeof limits === "object" ? limits[key] : undefined; + return typeof value === "number" && Number.isFinite(value) ? value : 0; + }; // Every field is coerced, so a missing or malformed snapshot reads as zero // rather than undefined: a script doing arithmetic on it gets 0, not NaN. return deepFreeze({ @@ -217,6 +222,12 @@ const BOOTSTRAP = String.raw` total: read("total"), cost: read("cost"), agents: read("agents"), + limits: { + concurrency: readLimit("concurrency"), + maxAgentCalls: readLimit("maxAgentCalls"), + callsUsed: readLimit("callsUsed"), + callsRemaining: readLimit("callsRemaining"), + }, }); }