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."
endView on GitHub (pinned to e69894adb9)
Solutions
- Pass plain non-negative numbers (0 is allowed) — for refunds/credits use the transaction side, not budget amounts.
- Clamp or correct the source value before calling: amount = [amount.to_f, 0.0].max when a floor of zero is the intended behavior.
- 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.
- 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
- Send amounts as plain JSON numbers ≥ 0; encode credits/refunds elsewhere, never as negative budget amounts.
- Floor computed values with [value, 0.0].max only when zero-flooring is actually intended business behavior.
- Remember the schema's minimum: 0 is descriptive (strict_mode? false) — enforce non-negativity at the call site.
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
- Each categories entry needs a category name or id.
- '#{ref}' is the unallocated remainder of budgeted_spending a
- Invalid month: #{raw}. Use YYYY-MM or MMM-YYYY.
- Category '#{ref}' not found. Use get_categories to list cate
- Assistant exceeded the tool-call limit of #{max_tool_call_it
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/cf7785194fff940f.
Report an issue: GitHub.