You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
lib/src/main/kotlin/dev/arcp/runtime/ARCPRuntime.kt — wire BudgetRegistry into job submission and into the metric ingestion path; surface BUDGET_EXHAUSTED as Nack or tool.error when a counter reaches zero.
lease/CostBudget.kt (new) — define Currency, BudgetAmount, CostBudget. Use java.math.BigDecimal for the decimal value. Hand-write BudgetAmountSerializer : KSerializer<BudgetAmount> that encodes/decodes the wire string "USD:5.00" (mirror the ErrorCodeSerializer pattern in error/ErrorCode.kt).
lease/BudgetCounter.kt (new) — mutable counter:
Internal ConcurrentHashMap<Currency, BigDecimal> initialised from the CostBudget.
consume(amount): require amount.value >= 0 (negative values rejected, no decrement, per §9.6); subtract from the matching currency; if the post-decrement value is ≤ BigDecimal.ZERO, return Outcome.Exhausted(currency). Otherwise Outcome.Ok.
isExhausted(): true iff any currency's remaining ≤ 0.
lease/BudgetRegistry.kt (new) — ConcurrentHashMap<JobId, BudgetCounter> plus the four methods above. terminate(jobId) removes the counter so jobs are not leaked across submissions.
lease/LeaseSubset.kt (new) — implement §9.4 / §10 rule: for every currency in child, the child amount must be ≤ the parent's remaining amount (caller passes the remaining as a CostBudget). Return false if a currency in child is absent from parent (children cannot introduce currencies the parent did not authorise).
error/ARCPException.kt — add public class BudgetExhausted(public val currency: Currency, public val jobId: JobId, message: String = "budget exhausted for ${currency.code} on $jobId") : ARCPException(message) { override val code = ErrorCode.BUDGET_EXHAUSTED }.
runtime/ARCPRuntime.kt —
Add private val budgets: BudgetRegistry = BudgetRegistry() constructor param (default).
When handling JobSubmit (see Implement agent versioning name@version #23), parse lease_request["cost.budget"] into a CostBudget and call budgets.register(jobId, costBudget).
Extend handleEnvelope to recognise incoming Metric envelopes whose name starts with "cost." (excluding "cost.budget.remaining") and whose unit matches a budgeted Currency. For each such metric, call budgets.consume(...). On Outcome.Exhausted, reply with a Nack (or JobFailed, see §9.6) carrying ErrorCode.BUDGET_EXHAUSTED.
After every material decrement, optionally emit a Metric(name = "cost.budget.remaining", value = …, unit = currency.code) envelope per §9.6.
On terminal job state, call budgets.terminate(jobId).
messages/Telemetry.kt — append public const val COST_BUDGET_REMAINING: String = "cost.budget.remaining" to StandardMetrics.
samples/src/main/kotlin/com/arcp/samples/costbudget/Main.kt (new) — emit three cost.inference metrics that sum to slightly more than the configured USD:1.00 budget; assert the third one triggers BUDGET_EXHAUSTED and the runtime emits a final cost.budget.remaining metric of ≤ 0. Print outcome.
samples/build.gradle.kts — register "runCostBudget" to "com.arcp.samples.costbudget.MainKt".
tests/src/test/kotlin/dev/arcp/tests/CostBudgetTest.kt (new) — end-to-end via HarnessFixture exercising budget enforcement at the client/runtime boundary.
lib/src/test/kotlin/dev/arcp/error/ErrorCodeTest.kt — extend the entries loop coverage to include BUDGET_EXHAUSTED and assert retryableByDefault == false.
tests/src/test/kotlin/dev/arcp/tests/CostBudgetTest.kt — drive a job with USD:0.10 budget; emit two cost.inference metrics summing past the budget; assert the runtime returns BUDGET_EXHAUSTED and terminate(jobId) cleared the counter.
ARCP v1.1 §9.6 introduces
cost.budgetas a runtime-enforced lease capability with counters. Not implemented.Steps:
CostBudgetcapability type (max,unit,consumed) tolib/src/main/kotlin/dev/arcp/lease/ARCPRuntime— increments on metric events tagged as costconsumed >= max, raiseBUDGET_EXHAUSTEDerror (see related error-code issue)samples/CostBudget/mirroringtypescript-sdk/examples/cost-budget/Reference: spec §9.6, §13.5.
Audit reference: ARCP SDK Audit v1.1 (2026-05-19)
Implementation prompt
Files to touch
lib/src/main/kotlin/dev/arcp/lease/CostBudget.kt(new) —CostBudgetvalue type,BudgetAmountparser, currency math.lib/src/main/kotlin/dev/arcp/lease/BudgetCounter.kt(new) — mutable counter that decrements on cost metric events.lib/src/main/kotlin/dev/arcp/lease/BudgetRegistry.kt(new) —JobId → BudgetCountermap with lifecycle hooks for job acceptance and termination.lib/src/main/kotlin/dev/arcp/lease/LeaseSubset.kt(new) —subsumes(parent: List<CostBudget>, child: List<CostBudget>): Booleanper §9.4 / §10.lib/src/main/kotlin/dev/arcp/error/ErrorCode.kt— addBUDGET_EXHAUSTED(depends on Add BUDGET_EXHAUSTED, AGENT_VERSION_NOT_AVAILABLE error codes #26).lib/src/main/kotlin/dev/arcp/error/ARCPException.kt— addARCPException.BudgetExhaustedsubclass.lib/src/main/kotlin/dev/arcp/runtime/ARCPRuntime.kt— wireBudgetRegistryinto job submission and into themetricingestion path; surfaceBUDGET_EXHAUSTEDasNackortool.errorwhen a counter reaches zero.lib/src/main/kotlin/dev/arcp/messages/Telemetry.kt— addStandardMetrics.COST_BUDGET_REMAININGconstant ("cost.budget.remaining"); keepCOST_USDetc untouched.lib/src/test/kotlin/dev/arcp/lease/CostBudgetTest.kt(new).lib/src/test/kotlin/dev/arcp/lease/BudgetCounterTest.kt(new).lib/src/test/kotlin/dev/arcp/lease/LeaseSubsetTest.kt(new).lib/src/test/kotlin/dev/arcp/error/ErrorCodeTest.kt— extend coverage forBUDGET_EXHAUSTED.samples/src/main/kotlin/com/arcp/samples/costbudget/Main.kt(new).samples/build.gradle.kts— registerrunCostBudget.tests/src/test/kotlin/dev/arcp/tests/CostBudgetTest.kt(new).Confirm with
ls /Users/nficano/code/arpc/kotlin-sdk/lib/src/main/kotlin/dev/arcp/(nolease/package exists yet — create it).Public API additions (package
dev.arcp.lease)Step-by-step changes
lease/CostBudget.kt(new) — defineCurrency,BudgetAmount,CostBudget. Usejava.math.BigDecimalfor the decimal value. Hand-writeBudgetAmountSerializer : KSerializer<BudgetAmount>that encodes/decodes the wire string"USD:5.00"(mirror theErrorCodeSerializerpattern inerror/ErrorCode.kt).lease/BudgetCounter.kt(new) — mutable counter:ConcurrentHashMap<Currency, BigDecimal>initialised from theCostBudget.consume(amount): requireamount.value >= 0(negative values rejected, no decrement, per §9.6); subtract from the matching currency; if the post-decrement value is≤ BigDecimal.ZERO, returnOutcome.Exhausted(currency). OtherwiseOutcome.Ok.isExhausted(): true iff any currency's remaining ≤ 0.lease/BudgetRegistry.kt(new) —ConcurrentHashMap<JobId, BudgetCounter>plus the four methods above.terminate(jobId)removes the counter so jobs are not leaked across submissions.lease/LeaseSubset.kt(new) — implement §9.4 / §10 rule: for every currency inchild, the child amount must be≤the parent's remaining amount (caller passes the remaining as aCostBudget). Returnfalseif a currency inchildis absent fromparent(children cannot introduce currencies the parent did not authorise).error/ErrorCode.kt— addBUDGET_EXHAUSTED("BUDGET_EXHAUSTED", false)(also see Add BUDGET_EXHAUSTED, AGENT_VERSION_NOT_AVAILABLE error codes #26).error/ARCPException.kt— addpublic class BudgetExhausted(public val currency: Currency, public val jobId: JobId, message: String = "budget exhausted for ${currency.code} on $jobId") : ARCPException(message) { override val code = ErrorCode.BUDGET_EXHAUSTED }.runtime/ARCPRuntime.kt—private val budgets: BudgetRegistry = BudgetRegistry()constructor param (default).JobSubmit(see Implement agent versioning name@version #23), parselease_request["cost.budget"]into aCostBudgetand callbudgets.register(jobId, costBudget).handleEnvelopeto recognise incomingMetricenvelopes whosenamestarts with"cost."(excluding"cost.budget.remaining") and whoseunitmatches a budgetedCurrency. For each such metric, callbudgets.consume(...). OnOutcome.Exhausted, reply with aNack(orJobFailed, see §9.6) carryingErrorCode.BUDGET_EXHAUSTED.Metric(name = "cost.budget.remaining", value = …, unit = currency.code)envelope per §9.6.budgets.terminate(jobId).messages/Telemetry.kt— appendpublic const val COST_BUDGET_REMAINING: String = "cost.budget.remaining"toStandardMetrics.samples/src/main/kotlin/com/arcp/samples/costbudget/Main.kt(new) — emit threecost.inferencemetrics that sum to slightly more than the configuredUSD:1.00budget; assert the third one triggersBUDGET_EXHAUSTEDand the runtime emits a finalcost.budget.remainingmetric of ≤ 0. Print outcome.samples/build.gradle.kts— register"runCostBudget" to "com.arcp.samples.costbudget.MainKt".tests/src/test/kotlin/dev/arcp/tests/CostBudgetTest.kt(new) — end-to-end viaHarnessFixtureexercising budget enforcement at the client/runtime boundary.Tests to add
lib/src/test/kotlin/dev/arcp/lease/CostBudgetTest.ktBudgetAmount.parse("USD:5.00").render() == "USD:5.00".BudgetAmount.parse("USD5.00")throws.CostBudget(listOf(parse("USD:1"), parse("USD:2")))throws."\"USD:5.00\"".lib/src/test/kotlin/dev/arcp/lease/BudgetCounterTest.ktUSD:1.00, consumeUSD:0.30, remaining =0.70.USD:0.10, consumeUSD:0.10→Outcome.Exhausted(USD).USD:-0.10throwsIllegalArgumentException.lib/src/test/kotlin/dev/arcp/lease/LeaseSubsetTest.ktUSD:5, childUSD:2→ true.USD:5, childUSD:10→ false.USD:5, childEUR:1→ false.lib/src/test/kotlin/dev/arcp/error/ErrorCodeTest.kt— extend the entries loop coverage to includeBUDGET_EXHAUSTEDand assertretryableByDefault == false.tests/src/test/kotlin/dev/arcp/tests/CostBudgetTest.kt— drive a job withUSD:0.10budget; emit twocost.inferencemetrics summing past the budget; assert the runtime returnsBUDGET_EXHAUSTEDandterminate(jobId)cleared the counter.Verification commands
Run from
/Users/nficano/code/arpc/kotlin-sdk:Acceptance
dev.arcp.leaseexists withCurrency,BudgetAmount,CostBudget,BudgetCounter,BudgetRegistry,LeaseSubset.BudgetAmountparses and renders thecurrency:decimalwire form per §9.6.BudgetCounter.consume(...)rejects negative values and returnsExhaustedat zero.LeaseSubset.subsumes(...)enforces per §9.4 / §10 — children may not exceed parent or add new currencies.ErrorCode.BUDGET_EXHAUSTEDexists withretryableByDefault = falseand round-trips (depends on Add BUDGET_EXHAUSTED, AGENT_VERSION_NOT_AVAILABLE error codes #26).ARCPRuntimedecrements counters on incoming cost metrics and replies withBUDGET_EXHAUSTEDwhen a counter reaches zero.StandardMetrics.COST_BUDGET_REMAININGconstant is exposed.samples/costbudget/Main.ktruns via./gradlew :samples:runCostBudget../gradlew checkgreen.