Skip to content

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.

: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.