antiwork/gumroad · error · Ai::StoreAgentService::Error

Message is required

Error message

Message is required

What it means

Raised as Ai::StoreAgentService::Error from #build_conversation (app/services/ai/store_agent_service.rb:1201). The service assembles an Anthropic Messages API conversation from client-supplied chat history: it keeps only non-blank messages with role 'user' or 'assistant', then drops leading assistant messages (the web chat opens with a canned assistant greeting) because Anthropic requires the conversation to start with a user message. If the filtered history is empty or the last remaining message is not a 'user' turn, there is no valid user turn to send and the service raises 'Message is required'.

Solutions

  1. Ensure the request includes at least one message with role exactly 'user' and non-blank content — typically the buyer's latest chat input.
  2. Make the last element of the messages array the user's new message; append it before calling the service rather than relying on the service to inject it.
  3. Filter client-side before calling: drop blank-content messages and validate every role is 'user' or 'assistant'.
  4. If you pass the chat window, verify the windowed slice still contains a user turn (window = Array(messages).last(MAX_HISTORY_MESSAGES)).
  5. In the caller, rescue Ai::StoreAgentService::Error and map it to a 400 'message is required' response instead of a 500.

Example fix

// before
result = Ai::StoreAgentService.new(seller:, messages: params[:messages]).call

// after
messages = Array(params[:messages]).select { |m| %w[user assistant].include?(m[:role].to_s) && m[:content].to_s.strip.present? }
raise ArgumentError, "Message is required" unless messages.any? { |m| m[:role] == "user" }
result = Ai::StoreAgentService.new(seller:, messages:).call
Defensive patterns

Strategy: validation

Validate before calling

def valid_agent_history?(messages)
  window = Array(messages).last(MAX_HISTORY_MESSAGES)
  kept = window.select do |m|
    %w[user assistant].include?((m[:role] || m["role"]).to_s) &&
      (m[:content] || m["content"]).to_s.strip.present?
  end
  kept.any? { |m| (m[:role] || m["role"]) == "user" } && (m = kept.last) && (m[:role] || m["role"]) == "user"
end

raise ArgumentError, "Message is required" unless valid_agent_history?(messages)

Type guard

# Ruby guard mirroring Ai::StoreAgentService#build_conversation filtering
def agent_history_has_user_turn?(messages)
  Array(messages).any? { |m| m[:role].to_s == "user" && m[:content].to_s.strip.present? }
end

Try / catch

begin
  Ai::StoreAgentService.new(seller:, messages:).call
rescue Ai::StoreAgentService::Error => e
  render json: { error: e.message }, status: :bad_request # 'Message is required' is caller input, not a 500
end

Prevention

When it happens

Trigger: Calling the store agent with messages: [] or messages whose :content is blank/whitespace-only (blank content is filtered out by `next if content.blank?`); sending only assistant-role turns (all dropped by drop_while); sending roles like 'system' or 'tool' (rejected by the %w[user assistant] allowlist); or sending history whose last non-blank message is an assistant turn (`history.last[:role] != "user"`).

Common situations: Frontend sends the greeting-only initial state of the chat; a client sends messages as JSON strings or with symbol/string key mismatch handled but content empty; the current-message input box is submitted empty; roles are typo'd ('User', 'bot'); or a conversation consisting solely of the canned assistant greeting plus a system-style message. Also hits when the MAX_HISTORY_MESSAGES window happens to contain only assistant turns.

Related errors


AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21). Data as JSON: /api/errors/b0dc0cb8b2b01dfc. Report an issue: GitHub.

Appendix: source

Thrown at app/services/ai/store_agent_service.rb:1201

        next unless %w[user assistant].include?(role)

        proposal_state = msg[:proposal_state] || msg["proposal_state"]
        state_suffix =
          if role == "assistant" && proposal_state.present?
            "\n\n[Server proposal state: #{proposal_state}]"
          else
            ""
          end
        limit = index == current_message_index ? MAX_CURRENT_MESSAGE_LENGTH : MAX_MESSAGE_LENGTH
        content = content.truncate(limit - state_suffix.length, omission: "...")
        { role:, content: "#{content}#{state_suffix}" }
      end

      # Anthropic's Messages API requires the conversation to START with a user message. The web chat
      # always opens with a canned assistant greeting (and a turn could begin with other leading
      # assistant turns), so drop any leading assistant messages before the first user message.
      history = history.drop_while { |m| m[:role] != "user" }
      raise Error, "Message is required" if history.empty? || history.last[:role] != "user"

      history
    end

    # Assemble the system prompt with the live read/write endpoint manifests embedded, so the model
    # is told exactly which endpoint ids exist and what each does.
    def system_prompt
      format(
        SYSTEM_PROMPT_HEADER,
        reads: Ai::StoreAgentApiCatalog.manifest(:read),
        writes: Ai::StoreAgentApiCatalog.manifest(:write),
      )
    end

    # Two generic API tools drive the whole catalog. `complete_turn` is a terminal marker: unlike
    # the API tools it never runs an operation, and the service validates it before accepting the
    # model's text as the final seller-facing reply.
    def run_tool(name:, arguments:)

View on GitHub (pinned to afeacbd394)