From a5b4799653ca1bb9ae93b1e525511b0a44be3933 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 1 Sep 2026 14:23:52 +0200 Subject: [PATCH] docs: correct the per-run cap guidance on the Deep Agents page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The page told readers to key the paid-call cap on `config["configurable"]["thread_id"]` (or `run_id`). Probing the real graph (langgraph 1.2 / deepagents 0.7) shows there is no run id anywhere in a tool's config — only `thread_id`, checkpoint bookkeeping, and whatever the caller passed. Since `thread_id` is stable for a whole conversation, following that guidance yields a per-conversation cap that never resets, not the per-run cap the section promises. Caught by aaitor reviewing the companion tutorial (nevermined-io/tutorials#61), where the same bug was live. Rewrites the section so the caller supplies a per-run nonce, shows the agent reporting which scope is actually in force, and adds the two details that bite in production: refund on PaymentRequiredError, and bound the counter map in a long-running server. Also records that InjectedState is not the tidier alternative it looks like — it exposes the subagent's own isolated conversation, reset on every task() hop, so it cannot see sibling delegations within one turn. `mintlify broken-links` clean. --- integrate/add-to-your-agent/deep-agents.mdx | 35 +++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/integrate/add-to-your-agent/deep-agents.mdx b/integrate/add-to-your-agent/deep-agents.mdx index c6c08fd..b4d9227 100644 --- a/integrate/add-to-your-agent/deep-agents.mdx +++ b/integrate/add-to-your-agent/deep-agents.mdx @@ -120,14 +120,43 @@ These are properties of the harness, not bugs. Both are worth handling before yo message may settle credits more than once. -Cap it explicitly rather than trusting the model to be frugal. Count paid calls per run, keyed on `config["configurable"]["thread_id"]` (or `run_id`), and return a plain refusal once the cap is hit: +Cap it explicitly rather than trusting the model to be frugal — but note **where a run's identity has to come from**. + +LangGraph does *not* put a run id in `config["configurable"]`. A tool sees only `thread_id`, checkpoint bookkeeping, and whatever the caller passed (verified against langgraph 1.2 / deepagents 0.7). Since `thread_id` is stable for a whole conversation, keying a "per-run" cap on it silently makes it per-*conversation*: after N paid calls the tool refuses forever, however many new questions the user asks. + +So the **caller** declares the run — it is the only party that knows where one ends: + +```python +# buyer side: a fresh nonce per run, alongside the token +"config": {"configurable": { + "payment_token": token, + "nvm_run_id": str(uuid.uuid4()), +}} +``` ```python +# agent side: key on the nonce, fall back to thread_id, and say which is in force if not budget.try_consume(config): - return "BUDGET_EXHAUSTED: this run already used its paid-call allowance." + if budget.scope_of(config) == "run": + return "BUDGET_EXHAUSTED: this run already used its paid-call allowance." + return ("BUDGET_EXHAUSTED: this conversation already used its allowance. " + "Pass a per-run `nvm_run_id` to scope the cap to one request.") ``` -Refund the reservation when a call raises `PaymentRequiredError` — otherwise a user who authorizes mid-run gets fewer paid calls than they paid for. +Two details that are easy to miss: + +- **Refund the reservation when a call raises `PaymentRequiredError`** — otherwise a user who authorizes mid-run gets fewer paid calls than they paid for. +- **Bound the counter map.** The agent is a long-running server, so a plain dict keyed on run or thread grows for the life of the process. An LRU with a fixed ceiling is enough; evicting a key only refills that budget, so the worst case is a long-idle caller getting a fresh allowance rather than an over-charge. + +A browser chat UI whose proxy injects only the token will fall into the conversation-scoped case. That is the safe direction to fail — it under-spends, never over-spends — as long as the refusal message says so instead of promising a reset that will not happen. + + + Counting from graph state via `InjectedState` looks like a tidier + alternative, and it does work inside a subagent tool — but it exposes the + **subagent's own** isolated conversation, which resets on every `task()` + hop. It therefore cannot see sibling delegations within a single turn, + which is exactly the case the cap exists for. + **Two LLM layers can paraphrase the paid tool's output.** The subagent relays