JuliusBrussee/caveman · error

cave_subagent_wallet_denomination_unavailable

cave_subagent_wallet_denomination_unavailable

Error message

cave_subagent_wallet_denomination_unavailable

What it means

When the parent run carries a BudgetMeter, each subagent dispatch must be funded from a wallet carved out of it: maxCostUsd for a usd-denominated meter, maxTokens otherwise. If the subagent runtime definition lacks the field matching the meter's denomination, walletAmount is undefined and the run aborts with cave_subagent_wallet_denomination_unavailable before the subagent starts.

Source

Thrown at packages/agent/src/runtime.ts:3891

  const calls = usage.calls.get(toolDefinition.name) ?? 0;
  if (calls >= runtime.maxCalls) throw new Error("cave_subagent_call_budget");
  // Reserve synchronously before any await so parallel Pi tool dispatch cannot
  // pass the same maxCalls check twice.
  usage.calls.set(toolDefinition.name, calls + 1);
  const depth = executionContext.depth;
  const depthLimit = Math.min(
    parentOptions.maxSubagentDepth ?? DEFAULT_SUBAGENT_DEPTH_LIMIT,
    ABSOLUTE_SUBAGENT_DEPTH_LIMIT,
  );
  if (depth + 1 > depthLimit) throw new Error("cave_subagent_depth_limit");
  // The wallet is carved here, still synchronously, for the same reason: two
  // subagents dispatched in one turn must not both be funded out of the same
  // remaining budget.
  const walletAmount = parentMeter === undefined
    ? undefined
    : parentMeter.denomination === "usd" ? runtime.maxCostUsd : runtime.maxTokens;
  if (parentMeter !== undefined && walletAmount === undefined) {
    throw new Error("cave_subagent_wallet_denomination_unavailable");
  }
  const carve = parentMeter === undefined || walletAmount === undefined
    ? undefined
    : parentMeter.carve(walletAmount);
  if (parentMeter !== undefined && carve === undefined) {
    throw new Error("cave_subagent_wallet_unavailable");
  }
  let releaseAdmission: (() => void) | undefined;
  try {
    releaseAdmission = admitSubagent(executionContext.invocationState);
    return await runSubagent({
      toolDefinition,
      runtime,
      task,
      signal,
      parentOptions,
      usage,
      executionContext,

View on GitHub (pinned to 766dce6b13)

Solutions

  1. Define both maxCostUsd and maxTokens on the subagent runtime so any meter denomination can fund it
  2. Or align the parent meter's denomination with the single field your subagent runtimes define

Example fix

// before — parent meter denomination is "usd"
runtime: { kind: "subagent", maxTokens: 50_000, /* no maxCostUsd */ }

// after
runtime: { kind: "subagent", maxTokens: 50_000, maxCostUsd: 0.5 }
Defensive patterns

Strategy: validation

Validate before calling

if (parentMeter !== undefined) {
  const requiredField = parentMeter.denomination === "usd" ? "maxCostUsd" : "maxTokens";
  if (tool.runtime[requiredField] === undefined) {
    throw new Error(`subagent ${tool.name} must define ${requiredField} for a ${parentMeter.denomination}-denominated meter`);
  }
}

Type guard

function canFundSubagent(
  runtime: { maxCostUsd?: number; maxTokens?: number },
  meter: { denomination: string },
): boolean {
  return meter.denomination === "usd"
    ? runtime.maxCostUsd !== undefined
    : runtime.maxTokens !== undefined;
}

Prevention

When it happens

Trigger: Parent meter denomination "usd" but the subagent runtime defines only maxTokens (no maxCostUsd); a token-denominated meter with a runtime that defines only maxCostUsd. Any dispatch while the meter is present fails until the field exists.

Common situations: Subagent definitions copy-pasted from a setup billed in the other denomination; switching the parent meter between token and usd modes without updating subagent runtimes; test fixtures defining only one of the two caps.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/8f4a7a497019642e. Report an issue: GitHub.