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

#{label} must be a non-negative number.

Error message

#{label} must be a non-negative number.

What it means

Raised by Assistant::Function::UpdateBudget#parse_amount! when Float(raw) succeeded but the value is not usable: !value.finite? (the string "Infinity" or "NaN" — Float() happily parses both) or value.negative?. The interpolated label names the offending field: "budgeted_spending", "expected_income", or "amount for '<category>'". Rescued by call into { success: false, error: "invalid_params" }.

Source

Thrown at app/models/assistant/function/update_budget.rb:150

      totals: {
        budgeted_spending: format_money(budget.budgeted_spending),
        expected_income: format_money(budget.expected_income),
        allocated_spending: format_money(budget.allocated_spending),
        available_to_allocate: format_money(budget.available_to_allocate)
      },
      updated_categories: updated,
      message: "Budget for #{budget.start_date.strftime('%B %Y')} updated."
    }
  rescue Assistant::Error => e
    error("invalid_params", e.message)
  rescue ActiveRecord::RecordInvalid => e
    error("validation_failed", e.record.errors.full_messages.join("; "))
  end

  private
    def parse_amount!(raw, label)
      value = Float(raw)
      raise Assistant::Error, "#{label} must be a non-negative number." if !value.finite? || value.negative?
      value
    rescue ArgumentError, TypeError
      raise Assistant::Error, "#{label} must be a non-negative number."
    end

    def find_budget_category!(budget, ref)
      ref = ref.to_s.strip
      raise Assistant::Error, "Each categories entry needs a category name or id." if ref.blank?

      category = valid_uuid?(ref) ? family.categories.find_by(id: ref) : nil
      category ||= family.categories.where("LOWER(name) = ?", ref.downcase).first

      if category.nil?
        if Category.all_uncategorized_names.any? { |name| name.casecmp?(ref) }
          raise Assistant::Error, "'#{ref}' is the unallocated remainder of budgeted_spending and cannot be set directly. Adjust budgeted_spending or category amounts instead."
        end
        raise Assistant::Error, "Category '#{ref}' not found. Use get_categories to list categories."
      end

View on GitHub (pinned to e69894adb9)

Solutions

  1. Pass plain non-negative numbers (0 is allowed) — for refunds/credits use the transaction side, not budget amounts.
  2. Clamp or correct the source value before calling: amount = [amount.to_f, 0.0].max when a floor of zero is the intended behavior.
  3. If the LLM keeps producing negatives, reinforce the tool description ('Amounts are plain non-negative numbers') or the system prompt — the schema's minimum: 0 is not strictly enforced.
  4. For NaN/Infinity, fix the upstream computation that produced a non-finite value instead of stringifying it.

Example fix

# before
update_budget.call({ "budgeted_spending" => -6500.0 })

# after
update_budget.call({ "budgeted_spending" => 6500.0 })
# or, when flooring is intended:
update_budget.call({ "budgeted_spending" => [raw_value.to_f, 0.0].max })
Defensive patterns

Strategy: validation

Validate before calling

def valid_budget_amount?(raw)
  value = Float(raw) rescue nil
  value.is_a?(Float) && value.finite? && !value.negative?
end

Type guard

# Ruby
def non_negative_finite?(raw)
  v = Float(raw) rescue nil # handles strings and numerics; nil/Hash -> nil
  !v.nil? && v.finite? && v >= 0
end

Prevention

When it happens

Trigger: update_budget with budgeted_spending: -500 or expected_income: "-0.01" hits the negative branch at app/models/assistant/function/update_budget.rb:150; a categories entry with amount: "NaN" or "Infinity" (LLM-serialized non-finite numbers) hits the !finite? branch. Note the params schema declares minimum: 0 but strict_mode? is false, so enforcement falls to this method.

Common situations: LLM treating a refund/credit as a negative budget amount; arithmetic in the model producing -0.0 or NaN before calling the tool; JSON payload carrying string "Infinity" which JSON.parse itself would reject but tool-call argument parsing may pass through.

Related errors


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