JuliusBrussee/caveman · error

cave_subagent_runtime_missing

Error message

cave_subagent_runtime_missing

What it means

executeSubagent throws this when the tool definition it was handed does not carry runtime.kind === "subagent". It is an internal dispatch invariant: the subagent execution path may only run against tools declared with a subagent runtime. Hitting it as a user means the definition graph or tool wiring routed a normal tool into the nested-agent executor.

Source

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

  return Object.freeze({
    traceId: parent.traceId,
    spanId: randomBytes(8).toString("hex"),
    parentSpanId: parent.spanId,
  });
}

async function executeSubagent(
  toolDefinition: ToolDefinition,
  params: unknown,
  signal: AbortSignal | undefined,
  parentOptions: InternalRunOptions,
  usage: NestedUsage,
  executionContext: InternalExecutionContext,
  parentMeter: BudgetMeter | undefined,
  parentDeadlineAt: number | undefined,
): Promise<unknown> {
  const runtime = toolDefinition.runtime;
  if (runtime?.kind !== "subagent") throw new Error("cave_subagent_runtime_missing");
  if (params === null || typeof params !== "object" || Array.isArray(params) ||
      typeof (params as { task?: unknown }).task !== "string") {
    throw new Error("cave_subagent_arguments_invalid");
  }
  const task = (params as { task: string }).task;
  if (task.length > runtime.maxInputChars) throw new Error("cave_subagent_input_limit");
  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

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Create nested-agent tools with the framework's subagent() builder rather than hand-authoring ToolDefinition objects.
  2. Verify toolDefinition.runtime?.kind === "subagent" for every tool you route into nested execution before dispatch.
  3. Check for definition copies/spreads that omit the runtime field (it is non-enumerable or stripped in some clone paths).

Example fix

// before
const tool: ToolDefinition = { name: "helper", description: "...", execute: nestedRun };

// after
const tool = subagent({ name: "helper", description: "...", /* agent definition */ });
Defensive patterns

Strategy: type-guard

Validate before calling

if (tool.runtime?.kind !== "subagent") {
  throw new Error(`tool ${tool.name} lacks subagent runtime; build it with subagent()`);
}

Type guard

const hasSubagentRuntime = (t: ToolDefinition): boolean =>
  t.runtime?.kind === "subagent";

Try / catch

try {
  await executeNested(tool);
} catch (error) {
  if (error instanceof Error && error.message === "cave_subagent_runtime_missing") {
    // definition wiring bug — rebuild the tool with subagent(), do not retry
  } else throw error;
}

Prevention

When it happens

Trigger: A tool definition reaches executeSubagent without a runtime object, or with runtime.kind other than "subagent" (e.g. hand-built ToolDefinition objects passed where subagent() was intended, or a graph mutation stripped the runtime field).

Common situations: Building tool definitions manually instead of via the subagent() builder; spreading/copying a tool definition and dropping the runtime property; version drift between the builder API and runtime dispatcher.

Related errors


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