JuliusBrussee/caveman · error

cave_reasoning_usage_unavailable

cave_reasoning_usage_unavailable

Error message

cave_reasoning_usage_unavailable

What it means

Thrown before a model call when an efficiency plan is active (candidatePlan/lockedBuild) but the selected model or transport cannot report the reasoning-token split (reasoningUsageUnavailable). Plans budget reasoning tokens explicitly, so a provider that only reports aggregate usage cannot enforce the reasoning budget; the run refuses rather than metering an unknown split as zero.

Source

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

            );
          }
        }
        if (reservation !== undefined && reservation.length > 0) {
          markSpendIncomplete(reservation.map((item) => item.ledger));
          spendFailure ??= failure;
        }
      }
      finalMessage = message;
    };
    const streamFn: StreamFn = async (selected, context, streamOptions) => {
      if (options.signal?.aborted) {
        throw options.signal.reason ?? new Error("cave_run_aborted");
      }
      if (spendFailure) throw spendFailure;
      if (usageFailure) throw usageFailure;
      if (nestedUsage.incomplete) throw new Error("cave_nested_usage_incomplete");
      if (efficiencyPlan && reasoningUsageUnavailable) {
        throw new Error("cave_reasoning_usage_unavailable");
      }
      if (efficiencyPlan) {
        enforceSemanticBudgets(contextBill(lowered.ir), outputTokens, efficiencyPlan);
        if (reasoningTokens > efficiencyPlan.budgets.reasoning) {
          throw new Error("cave_reasoning_budget_exceeded");
        }
      }
      // The hard model-call ceiling is a stop condition, not a failure: ending
      // the run through the same graceful path as every other stop keeps the
      // partial work and the receipt intact. Checked before the
      // increment so exactly `maxModelCalls` calls are allowed.
      if (modelCalls >= maxModelCalls) {
        stopReason = "call_budget_exhausted";
        refusalPending = true;
        throw new Error("cave_run_stopped");
      }
      modelCalls++;
      // Between-calls stop point. Nothing is in flight here: the previous turn

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Resolve the run to the exact provider/model the plan was built for (validatePlanSelection already enforces identity — check definition.model)
  2. Upgrade the package so the provider adapter carries the reasoning split for your model
  3. If the model genuinely reports no reasoning, rebuild the plan against a model whose reasoning usage is observable
  4. Do not pass candidatePlan/lockedBuild for ad-hoc runs on models without reasoning reporting

Example fix

// before
const agent = defineAgent({
  instructions: SYS,
  model: "other/no-reasoning-report",
  tools: [t],
  reasoning: true,
});
await agent.run(input, { candidatePlan });

// after
const agent = defineAgent({
  instructions: SYS,
  model: "anthropic/claude-with-thinking", // reports reasoning split
  tools: [t],
  reasoning: true,
});
await agent.run(input, { candidatePlan });
Defensive patterns

Strategy: try-catch

Validate before calling

// Before running with a plan, confirm the resolved model matches the plan's
// provider/model (identity is what makes reasoning usage observable).
function assertPlanModelMatch(
  plan: { provider: string; model: string },
  definition: AgentDefinition,
  resolvedModel: { provider: string; id: string },
) {
  const declared = typeof definition.model === "string"
    ? definition.model
    : `${resolvedModel.provider}/${resolvedModel.id}`;
  if (declared !== `${plan.provider}/${plan.model}`) {
    throw new Error(`plan built for ${plan.provider}/${plan.model}, run resolves ${declared}`);
  }
}

Type guard

const isPlanModelMatch = (
  plan: { provider: string; model: string },
  resolved: { provider: string; id: string },
): boolean => plan.provider === resolved.provider && plan.model === resolved.id;

Try / catch

try {
  await agent.run(input, { candidatePlan });
} catch (e) {
  if (e instanceof Error && e.message === "cave_reasoning_usage_unavailable") {
    // re-run without the plan, or re-lock against a model reporting reasoning
  } else throw e;
}

Prevention

When it happens

Trigger: Running with a candidate plan or locked build on a reasoning-capable model whose provider/adapter does not return a reasoning breakdown, or through a caller streamFn that strips reasoning usage from responses.

Common situations: Locking a plan against one model (Anthropic with thinking) then resolving the run to a different model at runtime; a provider API change that stopped returning the reasoning field; an older adapter version that drops the reasoning split.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/ef903defbd9033bd. Report an issue: GitHub.