{"record":{"id":"745edbc21b1eee2b","repo":"we-promise/sure","slug":"assistant-exceeded-the-tool-call-limit-of-max-to","errorCode":null,"errorMessage":"Assistant exceeded the tool-call limit of #{max_tool_call_iterations} for one response","messagePattern":"Assistant exceeded the tool-call limit of #(.+?) for one response","errorType":"exception","errorClass":"Assistant::Responder::ToolCallLimitError","httpStatus":null,"severity":"error","filePath":"app/models/assistant/responder.rb","lineNumber":26,"sourceCode":"    @instructions = instructions\n    @function_tool_caller = function_tool_caller\n    @llm = llm\n  end\n\n  def on(event_name, &block)\n    listeners[event_name.to_sym] << block\n  end\n\n  def respond(previous_response_id: nil)\n    response, response_has_text = request_response(previous_response_id: previous_response_id)\n    any_response_has_text = response_has_text\n    in_flight_function_results = []\n    iteration = 0\n\n    while response.function_requests.any?\n      iteration += 1\n      if iteration > max_tool_call_iterations\n        raise ToolCallLimitError,\n              \"Assistant exceeded the tool-call limit of #{max_tool_call_iterations} for one response\"\n      end\n\n      function_tool_calls = function_tool_caller.fulfill_requests(response.function_requests)\n      function_results = function_tool_calls.map(&:to_result)\n      in_flight_function_results.concat(function_results)\n\n      emit(:response, {\n        id: response.id,\n        function_tool_calls: function_tool_calls\n      })\n\n      response, response_has_text = request_response(\n        function_results: provider_preserves_response_context? ? function_results : in_flight_function_results.dup,\n        previous_response_id: response.id\n      )\n      any_response_has_text ||= response_has_text\n    end","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/assistant/responder.rb#L8-L44","documentation":"Raised by Assistant::Responder#respond as ToolCallLimitError when the LLM keeps requesting function calls for more than max_tool_call_iterations round trips within a single user turn. The counter increments once per loop iteration (one LLM response with function_requests → tool execution → next LLM response); the default limit is 5, overridable via ENV['ASSISTANT_MAX_TOOL_CALL_ITERATIONS']. It is a runaway-loop guard so one turn cannot ping-pong with tools forever.","triggerScenarios":"The while loop at app/models/assistant/responder.rb:23-44 runs a 6th iteration: e.g. the model alternates get_budget → update_budget → get_budget → ... never emitting text; or a tool keeps returning an error payload the model keeps retrying; or the task legitimately needs more than 5 sequential tool round trips (default).","commonSituations":"Broad prompts requiring many sequential lookups (scan every account, then every month); tool results that are error hashes the model retries instead of giving up; instructions that encourage verification loops after each write; ASSISTANT_MAX_TOOL_CALL_ITERATIONS lowered or left at default while workflows grew.","solutions":["Raise the limit via ENV when the workflow legitimately needs more round trips: ASSISTANT_MAX_TOOL_CALL_ITERATIONS=10 (invalid or non-positive values fall back to 5).","Inspect the emitted :response events to see which tool the model loops on, then fix that tool's output — usually an error result it cannot recover from.","Tighten instructions so the model must answer after gathering data, and encourage batching (update_budget accepts multiple categories in one call).","Rescue Assistant::Responder::ToolCallLimitError in the caller and surface a friendly message instead of a raw 500."],"exampleFix":"# before\nresponder.respond\n\n# after — bound the turn and degrade gracefully\nbegin\n  responder.respond\nrescue Assistant::Responder::ToolCallLimitError\n  message.update!(content: \"I hit my tool-use limit trying to answer that. Could you narrow the request?\")\nend\n\n# and/or, in env, when the workflow legitimately needs more hops:\n# ASSISTANT_MAX_TOOL_CALL_ITERATIONS=10","handlingStrategy":"try-catch","validationCode":"limit = Integer(ENV.fetch(\"ASSISTANT_MAX_TOOL_CALL_ITERATIONS\", 5))\nraise ArgumentError, \"ASSISTANT_MAX_TOOL_CALL_ITERATIONS must cover this workflow\" if workflow_needs_more_hops_than?(limit)","typeGuard":null,"tryCatchPattern":"begin\n  responder.respond(previous_response_id: prev_id)\nrescue Assistant::Responder::ToolCallLimitError\n  # surface partial progress; the turn's tool calls were already emitted via :response events\n  message.update!(content: \"I hit my tool-use limit — please narrow the request or continue in a new message.\")\nend","preventionTips":["Set ASSISTANT_MAX_TOOL_CALL_ITERATIONS to cover your longest legitimate tool chain (invalid values silently fall back to 5).","Instrument which tool the model loops on via the :response events; fix error-result tool outputs the model keeps retrying.","Design tools to batch (e.g. update_budget accepts a categories array) so one turn needs fewer round trips."],"tags":["assistant","llm","tool-call","loop-guard","agent-loop"],"backgroundTag":"llm-tool-call-loop","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}