we-promise/sure · error · Assistant::Responder::EmptyResponseError

Assistant returned neither text nor tool calls

Error message

Assistant returned neither text nor tool calls

What it means

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.

Source

Thrown at app/models/assistant/responder.rb:46

      end

      function_tool_calls = function_tool_caller.fulfill_requests(response.function_requests)
      function_results = function_tool_calls.map(&:to_result)
      in_flight_function_results.concat(function_results)

      emit(:response, {
        id: response.id,
        function_tool_calls: function_tool_calls
      })

      response, response_has_text = request_response(
        function_results: provider_preserves_response_context? ? function_results : in_flight_function_results.dup,
        previous_response_id: response.id
      )
      any_response_has_text ||= response_has_text
    end

    raise EmptyResponseError, "Assistant returned neither text nor tool calls" unless any_response_has_text

    emit(:response, { id: response.id })
  end

  private
    attr_reader :message, :instructions, :function_tool_caller, :llm

    def request_response(function_results: [], previous_response_id: nil)
      response_has_text = false

      streamer = proc do |chunk|
        if chunk.type == "output_text" && chunk.data.present?
          response_has_text = true
          emit(:output_text, chunk.data)
        end
      end

      response = get_llm_response(

View on GitHub (pinned to e69894adb9)

Solutions

  1. Retry the turn once — transient empty completions usually succeed on re-ask; EmptyResponseError means no partial output was shown, so a retry is safe.
  2. 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.
  3. 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.
  4. Rescue Assistant::Responder::EmptyResponseError in the caller and show a retry affordance instead of a raw error.

Example fix

# before
responder.respond

# after — retry once, then degrade gracefully
begin
  attempts = (attempts || 0) + 1
  responder.respond
rescue Assistant::Responder::EmptyResponseError
  retry if attempts == 1
  raise
end
Defensive patterns

Strategy: try-catch

Try / catch

begin
  responder.respond
rescue Assistant::Responder::EmptyResponseError
  retry if (retries = (retries || 0) + 1) == 1 # nothing was shown, so a retry is safe
  raise
end

Prevention

When it happens

Trigger: 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").

Common situations: 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.

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/1f0a3b4a909765fd. Report an issue: GitHub.