Budgets — hierarchical, live-enforced
Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §7D
Cap tool calls and wall-clock per agent and per team, enforced while the run is in flight.
Hierarchical budgets are about a team: a parent hands a child a share of its remaining allowance, the child cannot exceed it, and the parent’s own ceiling accounts for everything its children spent. That arithmetic needs a runtime that owns the children. What this port has instead is a set of independent, single-scope limits.
What to use instead
Section titled “What to use instead”:max-turns bounds the loop — each turn is one provider call plus whatever tools it asked for — and
the run comes back with :status "incomplete" and :limit? true rather than an exception:
(require '[toolnexus.client :as client])
(def c (client/create-client {:base-url "https://api.anthropic.com" :style "anthropic" :model "claude-sonnet-4-6" :max-turns 8})) ; default is 10
(def r (client/run c "audit the repo" {:toolkit tk}))
(:turns r) ;; how many were actually used(:tool-call-count r) ;; total tool calls across the run(:limit? r) ;; true when the turn cap ended the run(:status r) ;; "done" | "pending" | "incomplete"(:usage r) ;; {:prompt-tokens ... :completion-tokens ... :total-tokens ...}Wall-clock is bounded per network call, not per run: :timeout on
remote-agent and on HTTP tools, and each MCP server’s own timeout.
There is no single knob that stops a run after N seconds.
For anything tighter, :on-event is the live seam. It is called synchronously inside the loop, so a
counter kept there sees every tool call as it happens:
(require '[toolnexus.client :as client] '[koine.time :as time])
(def spent (atom {:calls 0 :started (time/mono-ms)}))
(client/run c "audit the repo" {:toolkit tk :on-event (fn [e] (when (= "tool_call" (:type e)) (swap! spent update :calls inc)) (let [{:keys [calls started]} @spent] (when (or (> calls 40) (> (time/elapsed-ms started) 120000)) ;; your policy, your signal — the loop does not enforce this for you (deliver over-budget true))))})Note the honest limit: :on-event can observe and can flip a flag your own tools check, but it
cannot abort the loop mid-turn. The two enforcement points the port actually owns are :max-turns
and the per-call timeouts. To cap a delegated child, give it its own client with its own
:max-turns — the child’s spend is then bounded, but it is not deducted from the parent’s.
See also
Section titled “See also”toolnexus.client/create-client— where:max-turnsis settoolnexus.client/run—:turns,:tool-call-count,:limit?,:usage,:on-eventclient / metrics—:on-metric, for spend you want to record rather than capruntime / runtime— the missing runtime these budgets would belong to