{"record":{"id":"1f0a3b4a909765fd","repo":"we-promise/sure","slug":"assistant-returned-neither-text-nor-tool-calls","errorCode":null,"errorMessage":"Assistant returned neither text nor tool calls","messagePattern":"Assistant returned neither text nor tool calls","errorType":"exception","errorClass":"Assistant::Responder::EmptyResponseError","httpStatus":null,"severity":"error","filePath":"app/models/assistant/responder.rb","lineNumber":46,"sourceCode":"      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\n\n    raise EmptyResponseError, \"Assistant returned neither text nor tool calls\" unless any_response_has_text\n\n    emit(:response, { id: response.id })\n  end\n\n  private\n    attr_reader :message, :instructions, :function_tool_caller, :llm\n\n    def request_response(function_results: [], previous_response_id: nil)\n      response_has_text = false\n\n      streamer = proc do |chunk|\n        if chunk.type == \"output_text\" && chunk.data.present?\n          response_has_text = true\n          emit(:output_text, chunk.data)\n        end\n      end\n\n      response = get_llm_response(","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/assistant/responder.rb#L28-L64","documentation":"Raised by Assistant::Responder#respond as EmptyResponseError when the tool-call loop finished (last LLM response contained no function_requests) but no round trip in the whole turn ever produced output text — the streamer only flags response_has_text on output_text chunks with present data, and the final check also looks for response.messages with non-blank output_text.","triggerScenarios":"Every request_response in the turn yields neither streamable output_text chunks nor messages with output_text: provider returns content: \"\" with no tool calls; content filtered/removed by provider-side moderation; the model emits only whitespace; a streaming adapter that fails to surface output_text for this provider's chunk shape (chunk.type never equals \"output_text\").","commonSituations":"Provider outage or degraded mode returning empty completions; safety filters blanking a response; a new provider adapter whose stream chunk type differs (so text arrives but is never counted); reasoning-only models that put everything in reasoning fields with no output text.","solutions":["Retry the turn once — transient empty completions usually succeed on re-ask; EmptyResponseError means no partial output was shown, so a retry is safe.","Check the provider status page and the raw response captured for the turn (response.messages) to see whether content was filtered or merely mis-parsed.","If you maintain the provider adapter, verify its streamer emits chunks with type \"output_text\" and non-empty data — otherwise the responder counts the turn as textless.","Rescue Assistant::Responder::EmptyResponseError in the caller and show a retry affordance instead of a raw error."],"exampleFix":"# before\nresponder.respond\n\n# after — retry once, then degrade gracefully\nbegin\n  attempts = (attempts || 0) + 1\n  responder.respond\nrescue Assistant::Responder::EmptyResponseError\n  retry if attempts == 1\n  raise\nend","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  responder.respond\nrescue Assistant::Responder::EmptyResponseError\n  retry if (retries = (retries || 0) + 1) == 1 # nothing was shown, so a retry is safe\n  raise\nend","preventionTips":["Retry the turn once on empty responses — transient provider blanks usually clear immediately and no partial output was emitted.","If you add provider adapters, ensure the streamer emits chunks with type \"output_text\" and non-blank data so text is counted.","Track EmptyResponseError rate per provider — a spike usually means an outage or moderation filtering rather than a code bug."],"tags":["assistant","llm","empty-response","streaming","agent-loop"],"backgroundTag":"empty-llm-response","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}