we-promise/sure · error · Assistant::Error

External assistant returned an empty response.

Error message

External assistant returned an empty response.

What it means

Assistant::External#respond_to streams the external agent's reply into the assistant_message via append_text! inside the client.chat block. After the call returns, if assistant_message.content is still blank it raises Assistant::Error("External assistant returned an empty response.") — the agent was reachable and authorized but produced zero visible text (or only whitespace). The method's own rescue catches it: since response_completed is still false, cleanup_partial_response removes the empty message and the error is attached to the chat via chat.add_error, so the user sees it inline as a chat error, not a crash.

Source

Thrown at app/models/assistant/external.rb:60

        "External assistant is not configured. Set the URL and token in Settings > Self-Hosting or via environment variables."
    end

    unless self.class.allowed_user?(chat.user)
      raise Assistant::Error, "Your account is not authorized to use the external assistant."
    end

    client = build_client
    messages = build_conversation_messages

    model = client.chat(
      messages: messages,
      user: "sure-family-#{chat.user.family_id}"
    ) do |text|
      assistant_message.append_text!(text)
    end

    if assistant_message.content.blank?
      raise Assistant::Error, "External assistant returned an empty response."
    end

    response_completed = true
    assistant_message.update!(ai_model: model) if model.present?
  rescue Assistant::Error, ActiveRecord::ActiveRecordError => e
    cleanup_partial_response(assistant_message) unless response_completed
    chat.add_error(e)
  rescue => e
    Rails.logger.error("[Assistant::External] Unexpected error: #{e.class} - #{e.message}")
    cleanup_partial_response(assistant_message) unless response_completed
    chat.add_error(Assistant::Error.new("Something went wrong with the external assistant. Check server logs for details."))
  end

  private

    def cleanup_partial_response(assistant_message)
      assistant_message&.destroy! if assistant_message&.persisted?
    rescue ActiveRecord::ActiveRecordError => e

View on GitHub (pinned to e69894adb9)

Solutions

  1. Check the external agent's own logs for the failed turn — the request reached it and came back content-less
  2. Test the agent directly with the same message (curl its chat endpoint) and confirm it emits non-empty text the bridge recognizes as output
  3. If the agent legitimately returns tool-call/structured output only, adjust it (or the bridge config) to include a final text frame
  4. If empty responses are transient, simply retry the message — the failed empty assistant_message is cleaned up automatically

Example fix

# before (external agent returns no text events)
client.chat(messages:, user:) { |text| assistant_message.append_text!(text) }
assistant_message.content.blank? # => true -> Assistant::Error, message purged

# after (agent-side: always emit a final text frame)
# in the agent's handler, after tool runs:
#   yield "Done. Summary: ..."  (non-empty final text)
# app-side guard before showing, optional:
raise Assistant::Error, "empty response" if assistant_message.content.blank? # already built-in; just retry
Defensive patterns

Strategy: fallback

Validate before calling

# Agent-side contract: always end a turn with a non-empty text frame
# Client-side: verify the endpoint yields text before wiring it in
resp = client.chat(messages: [ { role: "user", content: "ping" } ], user: "smoke") { |t| collected << t }
abort "agent produced no text" if collected.join.strip.empty?

Type guard

def agent_returns_text?(client)
  buffer = +""
  client.chat(messages: [ { role: "user", content: "ping" } ]) { |t| buffer << t }
  !buffer.strip.empty?
end

Try / catch

rescue Assistant::Error => e
  if e.message.include?("empty response")
    # transient-shaped: safe to let the user retry once; empty message already purged
    chat.add_error(e) # shown inline; UI offers "Try again"
  else
    raise
  end
end

Prevention

When it happens

Trigger: The external agent responds 200 with an empty body or an SSE stream containing no text events; the agent replies only with tool-call/function frames the bridge doesn't map to text; a proxy (nginx empty response, 204) between app and agent stripping the body; the agent hit its own internal error and closed the stream cleanly with no content; a model misconfiguration on the agent side producing empty completions.

Common situations: First bring-up of a custom agent whose response schema doesn't match what the client parses; agent gateway returning 204 on overload; streaming responses disabled mid-deploy; prompts configured to return structured JSON only, yielding no natural-language text; intermittent provider outages that manifest as empty streams.

Related errors


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