src/arcp/_runtime/job.py:222 emits a cost.budget.remaining follow-up metric with "value": float(remaining), where remaining is a Decimal produced by Job.apply_cost_metric (see src/arcp/_runtime/job.py:78-80). The conversion float(Decimal) is the textbook example of why Decimal exists in the standard library: a budget of Decimal("0.1") + Decimal("0.2") becomes 0.30000000000000004 on the wire, and a client receiving the metric cannot recover the exact remaining amount. The same coercion happens implicitly for the incoming value at src/arcp/_runtime/job.py:209 via float(body.get("value", 0)), which is then re-wrapped as Decimal(str(value)) in apply_cost_metric — losing precision before the budget arithmetic even runs. For a v1.1 protocol that publishes currency amounts as currency:decimal strings (src/arcp/_messages/execution.py:53), exposing them as floats on the metric path is a step backward from the wire format itself.
Fix prompt: Keep Decimal end-to-end on the metric path. In JobContext.metric at src/arcp/_runtime/job.py:205, accept value as either Decimal | str | int | float and normalize via Decimal(str(value)) before calling apply_cost_metric. When emitting the follow-up metric at src/arcp/_runtime/job.py:218, serialize remaining as the canonical f"{remaining}" string with the existing currency rather than float(remaining), and update the consumer-side parsing helpers accordingly. If the wire format truly requires a JSON number for the metric body, document the precision contract explicitly in the spec section linked from EVENT_KINDS and round to the documented number of decimal places using Decimal.quantize. Add a property-based Hypothesis test that round-trips arbitrary Decimal budget amounts through emit/parse and asserts equality.
src/arcp/_runtime/job.py:222emits acost.budget.remainingfollow-up metric with"value": float(remaining), whereremainingis aDecimalproduced byJob.apply_cost_metric(seesrc/arcp/_runtime/job.py:78-80). The conversionfloat(Decimal)is the textbook example of whyDecimalexists in the standard library: a budget ofDecimal("0.1") + Decimal("0.2")becomes0.30000000000000004on the wire, and a client receiving the metric cannot recover the exact remaining amount. The same coercion happens implicitly for the incomingvalueatsrc/arcp/_runtime/job.py:209viafloat(body.get("value", 0)), which is then re-wrapped asDecimal(str(value))inapply_cost_metric— losing precision before the budget arithmetic even runs. For a v1.1 protocol that publishes currency amounts ascurrency:decimalstrings (src/arcp/_messages/execution.py:53), exposing them as floats on the metric path is a step backward from the wire format itself.Fix prompt: Keep
Decimalend-to-end on the metric path. InJobContext.metricatsrc/arcp/_runtime/job.py:205, acceptvalueas eitherDecimal | str | int | floatand normalize viaDecimal(str(value))before callingapply_cost_metric. When emitting the follow-up metric atsrc/arcp/_runtime/job.py:218, serializeremainingas the canonicalf"{remaining}"string with the existing currency rather thanfloat(remaining), and update the consumer-side parsing helpers accordingly. If the wire format truly requires a JSON number for the metric body, document the precision contract explicitly in the spec section linked fromEVENT_KINDSand round to the documented number of decimal places usingDecimal.quantize. Add a property-based Hypothesis test that round-trips arbitraryDecimalbudget amounts through emit/parse and asserts equality.