From cea324f719082136f47be14b3e63e6435195e894 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Tue, 3 Nov 2026 20:17:01 +0800 Subject: [PATCH] fix(ptc): report the per-minute call budget with the limit The model-visible limit diagnostic only stated the timeout-derived total, so the number could not be traced back to maxSubCallsPerMinute. --- packages/core/tool-executor-ptc/README.md | 2 +- packages/core/tool-executor-ptc/README.zh.md | 2 +- packages/core/tool-executor-ptc/src/errors.ts | 15 ++++++++++++--- packages/core/tool-executor-ptc/src/ptc.ts | 15 +++++++++------ packages/core/tool-executor-ptc/tests/ptc.spec.ts | 8 ++++---- .../session/ptc-tool-call-limit/session.v4.jsonl | 2 +- 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/core/tool-executor-ptc/README.md b/packages/core/tool-executor-ptc/README.md index 49f9844c7d..f8276ca784 100644 --- a/packages/core/tool-executor-ptc/README.md +++ b/packages/core/tool-executor-ptc/README.md @@ -112,7 +112,7 @@ Program-only SDK bindings: ##### Tool-call allowance exhausted ```markdown -Error: code run failed (abort): run_code exceeded its limit of N tool calls; call M was not dispatched. The run_code program may contain an infinite loop. Check loop termination or split the work into smaller run_code calls. +Error: code run failed (abort): run_code exceeded its limit of 2048 tool calls (maxSubCallsPerMinute 1024); call 2049 was not dispatched. The run_code program may contain an infinite loop. Check loop termination or split the work into smaller run_code calls. ``` #### Token effect diff --git a/packages/core/tool-executor-ptc/README.zh.md b/packages/core/tool-executor-ptc/README.zh.md index 1d0f623a31..842fb5c8ea 100644 --- a/packages/core/tool-executor-ptc/README.zh.md +++ b/packages/core/tool-executor-ptc/README.zh.md @@ -112,7 +112,7 @@ Program-only SDK bindings: ##### 工具调用额度耗尽 ```markdown -Error: code run failed (abort): run_code exceeded its limit of N tool calls; call M was not dispatched. The run_code program may contain an infinite loop. Check loop termination or split the work into smaller run_code calls. +Error: code run failed (abort): run_code exceeded its limit of 2048 tool calls (maxSubCallsPerMinute 1024); call 2049 was not dispatched. The run_code program may contain an infinite loop. Check loop termination or split the work into smaller run_code calls. ``` #### Token 影响 diff --git a/packages/core/tool-executor-ptc/src/errors.ts b/packages/core/tool-executor-ptc/src/errors.ts index 194f1e4fc1..3f1582f8ae 100644 --- a/packages/core/tool-executor-ptc/src/errors.ts +++ b/packages/core/tool-executor-ptc/src/errors.ts @@ -1,6 +1,14 @@ /** Program execution failures reported by PTC. @module */ import { HarnessError } from '@deepseek-ai/dsh-llm' +/** Tool-call allowance for one `run_code` program, with the per-minute budget its limit derives from. */ +export interface ToolCallAllowance { + /** Maximum dispatched tool-call attempts for the program; `Number.POSITIVE_INFINITY` without a resolved deadline. */ + limit: number + /** Configured attempts granted per minute of resolved runtime timeout. */ + maxSubCallsPerMinute: number +} + /** * Host-only abort reason for an exhausted tool-call allowance; the excess call is never dispatched. * The executor recognizes this class at settlement and reports CODE_RUN_FAILED to the model. @@ -8,11 +16,12 @@ import { HarnessError } from '@deepseek-ai/dsh-llm' export class ToolCallLimitExceededError extends Error { /** * Describe the rejected call and how to correct the program. - * @param limit - Timeout-derived maximum tool-call attempts for one run_code invocation. + * @param allowance - Exhausted allowance, including the per-minute budget behind its limit. * @param callNumber - One-based attempt rejected without invoking the tool. */ - constructor(limit: number, callNumber: number) { - super(`run_code exceeded its limit of ${limit} tool calls; call ${callNumber} was not dispatched. ` + constructor(allowance: ToolCallAllowance, callNumber: number) { + super(`run_code exceeded its limit of ${allowance.limit} tool calls ` + + `(maxSubCallsPerMinute ${allowance.maxSubCallsPerMinute}); call ${callNumber} was not dispatched. ` + 'The run_code program may contain an infinite loop. Check loop termination or split the work into smaller run_code calls.') this.name = 'ToolCallLimitExceededError' } diff --git a/packages/core/tool-executor-ptc/src/ptc.ts b/packages/core/tool-executor-ptc/src/ptc.ts index f3e6b555cb..6641ad56c2 100644 --- a/packages/core/tool-executor-ptc/src/ptc.ts +++ b/packages/core/tool-executor-ptc/src/ptc.ts @@ -24,7 +24,7 @@ import type { ToolExecutionPipeline } from '@deepseek-ai/dsh-tools/pipeline' import { toolErrorMessage } from '@deepseek-ai/dsh-tools/pipeline' import { scopeTarget } from '@deepseek-ai/dsh-scope' import { PtcCalls } from './ptc-calls.ts' -import { CodeRunFailedError, ToolCallLimitExceededError } from './errors.ts' +import { CodeRunFailedError, ToolCallLimitExceededError, type ToolCallAllowance } from './errors.ts' import type { Config } from './config.ts' import type { ToolDefinition, ToolExecutionResult, ToolRunContext } from '@deepseek-ai/dsh-tools' import type {} from './types.ts' @@ -380,7 +380,7 @@ export function createRunCodeTool( callSignal.addEventListener('abort', onOuterAbort, { once: true }) let toolCallAttempts = 0 - let maxSubCalls: number + let allowance: ToolCallAllowance // The per-run scheduler uses the registry's staged interface and follows // the same concurrency rules as the native loop. It also follows the // native loop's SEQUENCING: every ordered stage (the dispatch-start @@ -509,8 +509,8 @@ export function createRunCodeTool( } // Count before validation and queuing so repeated failed calls also exhaust the allowance. const n = ++toolCallAttempts - if (n > maxSubCalls) { - const error = new ToolCallLimitExceededError(maxSubCalls, n) + if (n > allowance.limit) { + const error = new ToolCallLimitExceededError(allowance, n) runController.abort(error) throw error } @@ -694,8 +694,11 @@ export function createRunCodeTool( ...policy !== undefined ? { sandboxPolicy: policy } : {}, ...args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {}, }) - maxSubCalls = spec.timeoutMs === null ? Infinity - : Math.floor(spec.timeoutMs / 60_000 * maxSubCallsPerMinute) + allowance = { + limit: spec.timeoutMs === null ? Number.POSITIVE_INFINITY + : Math.floor(spec.timeoutMs / 60_000 * maxSubCallsPerMinute), + maxSubCallsPerMinute, + } result = await runtime.run(spec) } finally { // Abort sub-dispatches and drain every in-flight dispatch before diff --git a/packages/core/tool-executor-ptc/tests/ptc.spec.ts b/packages/core/tool-executor-ptc/tests/ptc.spec.ts index ae07d5d75f..e766ea6f01 100644 --- a/packages/core/tool-executor-ptc/tests/ptc.spec.ts +++ b/packages/core/tool-executor-ptc/tests/ptc.spec.ts @@ -683,7 +683,7 @@ describe('the per-program tool-call limit', () => { runtime.behavior = async (request) => { const echo = request.bindings[0]!.functions.echo! for (let n = 1; n <= 2048; n++) await echo({ value: String(n) }) - const message = 'run_code exceeded its limit of 2048 tool calls; call 2049 was not dispatched. ' + const message = 'run_code exceeded its limit of 2048 tool calls (maxSubCallsPerMinute 1024); call 2049 was not dispatched. ' + 'The run_code program may contain an infinite loop. Check loop termination or split the work into smaller run_code calls.' await expect(echo({ value: 'rejected' })).rejects.toThrow(message) expect(request.signal?.aborted).toBe(true) @@ -719,7 +719,7 @@ describe('the per-program tool-call limit', () => { expect(request.timeoutMs).toBe(resolvedMs) const echo = request.bindings[0]!.functions.echo! for (let n = 0; n < limit; n++) await echo({ value: String(n) }) - await expect(echo({ value: 'rejected' })).rejects.toThrow(`limit of ${limit} tool calls; call ${limit + 1} was not dispatched`) + await expect(echo({ value: 'rejected' })).rejects.toThrow(`limit of ${limit} tool calls (maxSubCallsPerMinute 4); call ${limit + 1} was not dispatched`) expect(request.signal?.aborted).toBe(true) return { logs: [], error: { kind: 'abort', message: String(request.signal?.reason) } } } @@ -767,7 +767,7 @@ describe('the per-program tool-call limit', () => { void functions.active!({}).catch(() => { outcomes.push('active canceled') }) await entered.promise void functions.queued!({ value: 'queued' }).catch(() => { outcomes.push('queued abandoned') }) - await expect(functions.queued!({ value: 'excess' })).rejects.toThrow('limit of 2 tool calls; call 3 was not dispatched') + await expect(functions.queued!({ value: 'excess' })).rejects.toThrow('limit of 2 tool calls (maxSubCallsPerMinute 1); call 3 was not dispatched') expect(request.signal?.aborted).toBe(true) return { logs: [], error: { kind: 'abort', message: 'execution canceled' } } } @@ -825,7 +825,7 @@ describe('the per-program tool-call limit', () => { await expect(functions.echo!(undefined)).rejects.toThrow('lossless JSON') await expect(functions.echo!({})).rejects.toThrow() await expect(functions.fail!({})).rejects.toThrow('tool failed') - await expect(functions.echo!({ value: 'rejected' })).rejects.toThrow('limit of 3 tool calls; call 4 was not dispatched') + await expect(functions.echo!({ value: 'rejected' })).rejects.toThrow('limit of 3 tool calls (maxSubCallsPerMinute 3); call 4 was not dispatched') expect(request.signal?.aborted).toBe(true) return { logs: [], error: { kind: 'abort', message: String(request.signal?.reason) } } } diff --git a/snapshots/session/ptc-tool-call-limit/session.v4.jsonl b/snapshots/session/ptc-tool-call-limit/session.v4.jsonl index 51b54a4439..612fed4e80 100644 --- a/snapshots/session/ptc-tool-call-limit/session.v4.jsonl +++ b/snapshots/session/ptc-tool-call-limit/session.v4.jsonl @@ -22,7 +22,7 @@ {"type":"job/registered","data":{"jobId":"{{id:2}}","owner":"{{session:1}}","kind":"bash","label":"printf 'call\n' >> calls.txt","startedAt":1793676779672,"toolCall":{"callId":"call_limit:ptc:2","rootCallId":"call_limit"}}} {"type":"job/settled","data":{"jobId":"{{id:2}}","owner":"{{session:1}}","kind":"bash","label":"printf 'call\n' >> calls.txt","startedAt":1793676779672,"toolCall":{"callId":"call_limit:ptc:2","rootCallId":"call_limit"},"finishedAt":1793676779794,"status":"completed","cause":"producer","producerReported":true,"detail":"exit code: 0"}} {"type":"tool/ptc-dispatch","data":{"rootCallId":"call_limit","parentCallId":"call_limit","subCallId":"call_limit:ptc:2","name":"bash","arguments":{"command":"printf 'call\n' >> calls.txt","description":"Append one completed call"},"isError":false,"content":[{"type":"text","text":"(no output)"}],"meta":{"cwd":"{{cwd}}"}}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_limit"},"toolCallId":"call_limit","content":[{"type":"text","text":"Error: code run failed (abort): run_code exceeded its limit of 2 tool calls; call 3 was not dispatched. The run_code program may contain an infinite loop. Check loop termination or split the work into smaller run_code calls.\nFile sandbox: danger-full-access."}],"isError":true,"id":"{{message:5}}"},"error":{"name":"CodeRunFailedError","code":"CODE_RUN_FAILED"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_limit"},"toolCallId":"call_limit","content":[{"type":"text","text":"Error: code run failed (abort): run_code exceeded its limit of 2 tool calls (maxSubCallsPerMinute 1); call 3 was not dispatched. The run_code program may contain an infinite loop. Check loop termination or split the work into smaller run_code calls.\nFile sandbox: danger-full-access."}],"isError":true,"id":"{{message:5}}"},"error":{"name":"CodeRunFailedError","code":"CODE_RUN_FAILED"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_next","name":"run_code","arguments":"{\"code\":\"await tools.bash({ command: \\\"printf 'next\\\\n' >> calls.txt\\\", description: 'Append from a fresh program' }); return 'next run succeeded';\",\"description\":\"Verify the next program has fresh allowance\"}"}} {"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_next","parentCallId":"call_next","subCallId":"call_next:ptc:1","name":"bash","arguments":{"command":"printf 'next\n' >> calls.txt","description":"Append from a fresh program"}}} {"type":"job/registered","data":{"jobId":"{{id:3}}","owner":"{{session:1}}","kind":"bash","label":"printf 'next\n' >> calls.txt","startedAt":1793676780083,"toolCall":{"callId":"call_next:ptc:1","rootCallId":"call_next"}}}