{"record":{"id":"38e79d444709ed75","repo":"alibaba/spring-ai-alibaba","slug":"tool-call-limits-exceeded-threadcount-thread","errorCode":null,"errorMessage":"Tool call limits exceeded: ${threadCount}/${threadLimit} thread, ${runCount}/${runLimit} run, tool: ${toolName}","messagePattern":"Tool call limits exceeded: (.+?)/(.+?) thread, (.+?)/(.+?) run, tool: (.+?)","errorType":"exception","errorClass":"ToolCallLimitExceededException","httpStatus":null,"severity":"error","filePath":"spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/hook/toolcalllimit/ToolCallLimitHook.java","lineNumber":84,"sourceCode":"\tprivate String getRunCountKey() {\n\t\tString trackKey = toolName != null ? toolName : \"__all__\";\n\t\treturn RUN_COUNT_KEY_PREFIX + \"_\" + trackKey;\n\t}\n\n\t@Override\n\tpublic CompletableFuture<Map<String, Object>> beforeModel(OverAllState state, RunnableConfig config) {\n\t\t// Read current counts from context\n\t\tint threadCount = config.context().containsKey(getThreadCountKey())\n\t\t\t\t? (int) config.context().get(getThreadCountKey()) : 0;\n\t\tint runCount = config.context().containsKey(getRunCountKey())\n\t\t\t\t? (int) config.context().get(getRunCountKey()) : 0;\n\n\t\tboolean threadLimitExceeded = threadLimit != null && threadCount >= threadLimit;\n\t\tboolean runLimitExceeded = runLimit != null && runCount >= runLimit;\n\n\t\tif (threadLimitExceeded || runLimitExceeded) {\n\t\t\tif (exitBehavior == ExitBehavior.ERROR) {\n\t\t\t\tthrow new ToolCallLimitExceededException(\n\t\t\t\t\t\tthreadCount,\n\t\t\t\t\t\trunCount,\n\t\t\t\t\t\tthreadLimit,\n\t\t\t\t\t\trunLimit,\n\t\t\t\t\t\ttoolName\n\t\t\t\t);\n\t\t\t}\n\t\t\telse if (exitBehavior == ExitBehavior.END) {\n\t\t\t\tString message = buildLimitExceededMessage(threadCount, runCount, threadLimit, runLimit, toolName);\n\n\t\t\t\t// Do not copy old messages\n\t\t\t\tList<Message> messages = new ArrayList<>();\n\t\t\t\t// This new message will be appended to the messages list\n\t\t\t\tmessages.add(new AssistantMessage(message));\n\n\t\t\t\tMap<String, Object> updates = new HashMap<>();\n\t\t\t\tupdates.put(\"messages\", messages);\n\t\t\t\tupdates.put(\"jump_to\", JumpTo.end);","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/alibaba/spring-ai-alibaba/blob/f82da0b50f35744c13968191be2b1cd2452ef550/spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/hook/toolcalllimit/ToolCallLimitHook.java#L66-L102","documentation":"ToolCallLimitHook with ExitBehavior.ERROR throws ToolCallLimitExceededException in beforeModel when the configured thread limit and/or run limit on tool calls has been reached. The message embeds counts like '5/5 thread, 12/10 run, tool: web_search' so you can see which limit tripped and for which tool. This is the intended hard-stop that prevents the agent from continuing to burn tool invocations.","triggerScenarios":"Within one thread/run, the agent makes as many tool calls as threadLimit or runLimit for a given toolName (or all tools if unscoped), and the next model request arrives; hook configured with exitBehavior(ERROR).","commonSituations":"Agent loops retrying a failing tool until the limit trips; limits set too low for a legitimately long workflow; a single hook instance limiting all tools when per-tool limits were intended.","solutions":["Raise threadLimit/runLimit in the ToolCallLimitHook builder to a level suited to the task.","Catch ToolCallLimitExceededException and handle it (e.g. summarize progress and end the run gracefully) or switch exitBehavior to a non-ERROR mode.","Fix the underlying loop causing excessive tool calls (e.g. retry without progress) so limits are not reached."],"exampleFix":"// before\nToolCallLimitHook hook = ToolCallLimitHook.builder().threadLimit(3).exitBehavior(ExitBehavior.ERROR).build();\n// after\nToolCallLimitHook hook = ToolCallLimitHook.builder().threadLimit(20).exitBehavior(ExitBehavior.ERROR).build();","handlingStrategy":"try-catch","validationCode":"// before running the agent, size the limits to expected usage\nint expectedCalls = estimateToolCalls(task);\nif (expectedCalls >= configuredThreadLimit) { raise or adjust limits; }","typeGuard":null,"tryCatchPattern":"try { agent.invoke(input); } catch (ToolCallLimitExceededException e) {\n    log.warn(\"Tool limit hit: {}\", e.getMessage());\n    return partialResultOrSummary();\n}","preventionTips":["Set limits with headroom above expected tool-call counts.","Catch ToolCallLimitExceededException at the agent boundary and degrade gracefully.","Monitor tool-call counts to detect runaway loops early."],"tags":["java","agent","rate-limit"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"f82da0b50f35744c13968191be2b1cd2452ef550","analyzedAt":"2026-09-09T15:32:42.421Z","contentChangedAt":"2026-09-09T15:32:42.421Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}