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'.

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)

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.