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 => eView on GitHub (pinned to e69894adb9)
Solutions
- Check the external agent's own logs for the failed turn — the request reached it and came back content-less
- Test the agent directly with the same message (curl its chat endpoint) and confirm it emits non-empty text the bridge recognizes as output
- If the agent legitimately returns tool-call/structured output only, adjust it (or the bridge config) to include a final text frame
- 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
- Make the external agent always emit a final non-empty text frame per turn
- Run a smoke chat against new agent versions before enabling them for users
- Ensure tool-only turns still end with a human-readable summary from the agent
- Treat empty streams as retriable but investigate agent logs after repeated hits
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
- Assistant returned neither text nor tool calls
- No LLM provider configured that supports model '#{requested_
- External assistant is not configured. Set the URL and token
- Your account is not authorized to use the external assistant
- External assistant connection was interrupted.
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/755400658be20754.
Report an issue: GitHub.