Hmbown/CodeWhale · error · anyhow::Error

Sub-agent token budget scope must not be empty

Error message

Sub-agent token budget scope must not be empty

What it means

Thrown by `resolve_budget_scope` when a caller requests a sub-agent token-budget scope whose scope id is empty or whitespace-only. A scope id is the key under which spent tokens are aggregated and handback reserves are tracked, so an empty id cannot address any budget bucket.

Solutions

  1. Provide a non-empty scope id when requesting/setting the sub-agent token budget.
  2. Trim or reject empty scope ids at the caller's input boundary before invoking the budget API.
  3. Fall back to a well-known default scope name (e.g. the session or workflow id) when no explicit scope was supplied.

Example fix

// before
engine.set_token_budget(String::new(), 50_000);
// after
let scope = if scope_id.trim().is_empty() { session_id.clone() } else { scope_id };
engine.set_token_budget(scope, 50_000);
Defensive patterns

Strategy: validation

Validate before calling

fn valid_scope(scope_id: &str) -> bool { !scope_id.trim().is_empty() }
assert!(valid_scope(&scope_id), "budget scope id must be non-empty");

Type guard

fn as_scope_id(raw: &str) -> Option<&str> {
    let t = raw.trim();
    (!t.is_empty()).then_some(t)
}

Prevention

When it happens

Trigger: Calling the budget-resolution path (`resolve_budget_scope` via the entry that maps it at mod.rs:5247) with `scope_id` set to `""` or only whitespace, typically because the caller derived the scope id from an unset field, an empty workflow name, or an untrimmed user input.

Common situations: A workflow/tool forgot to set the budget scope id; a config value was parsed as empty; programmatic callers passing `String::new()` as a default scope.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/b23d0706b0e6c5b6. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tools/subagent/mod.rs:5247

        // cap is stored on the worker profile and checked separately; creating
        // a new pool here would make its spend disappear from the parent.
        let scope = self.inherited_budget_scope(parent_run_id).or_else(|| {
            narrow_optional_limit(
                self.default_token_budget,
                positive_token_budget(requested_budget),
            )
            .map(|limit| (worker_id.to_string(), limit))
        });

        let Some((scope_id, limit)) = scope else {
            return Ok(None);
        };
        self.resolve_budget_scope(scope_id, limit).map(Some)
    }

    fn resolve_budget_scope(&self, scope_id: String, limit: u64) -> Result<AgentUsageBudgetScope> {
        if scope_id.trim().is_empty() {
            return Err(anyhow!("Sub-agent token budget scope must not be empty"));
        }
        let spent = self.aggregate_budget_spent(&scope_id);
        let remaining = limit.saturating_sub(spent);
        let work_remaining = remaining
            .saturating_sub(budget_handback::token_reserve(limit))
            .saturating_sub(self.reserved_handback_tokens(&scope_id, Some(&scope_id)));
        if work_remaining < MIN_SUBAGENT_SPAWN_TOKEN_RESERVE {
            return Err(anyhow!(
                "Sub-agent token budget exhausted for scope {scope_id}: {spent}/{limit} tokens spent, {remaining} remaining (including reserved reporting allowance). Wait for the parent/Workflow to summarize results or start a fresh agent run."
            ));
        }
        Ok(AgentUsageBudgetScope {
            scope_id,
            limit,
            spent,
            remaining,
        })
    }

View on GitHub (pinned to 433685b202)