can1357/oh-my-pi · error · Error

invokeTool: delegation depth exceeded 8 (recursive invokeToo

Error message

invokeTool: delegation depth exceeded 8 (recursive invokeTool for "${name}"?)

What it means

invokeTool tracks delegation depth and hard-caps it at 8 to stop recursive tool invocation (a tool whose execution invokes itself, directly or via a chain of extensions/tools). Exceeding the cap throws this Error naming the tool.

Source

Thrown at packages/coding-agent/src/extensibility/extensions/runner.ts:587

		params: Record<string, unknown>,
		options?: {
			signal?: AbortSignal;
			onUpdate?: AgentToolUpdateCallback<TDetails>;
			depth?: number;
			/**
			 * The caller tool's own context. Reused for the native call so metadata the native tool
			 * reads — `toolCall` (write/edit LSP batch flushing) and provider metadata /
			 * `providerSafetyApproved` (computer) — is preserved. Falls back to a fresh session tool
			 * context only when the caller had none.
			 */
			callerContext?: AgentToolContext;
		},
	): Promise<AgentToolResult<TDetails>> {
		const resolved = this.#nativeToolResolver?.(name);
		if (!resolved) throw new Error(`invokeTool: no native built-in named "${name}" to delegate to`);
		const depth = options?.depth ?? 0;
		if (depth >= 8) {
			throw new Error(`invokeTool: delegation depth exceeded 8 (recursive invokeTool for "${name}"?)`);
		}
		const toolCallId = `invoke-${name}-${Date.now().toString(36)}-${depth}`;
		return (await resolved.tool.execute(
			toolCallId,
			params as never,
			options?.signal,
			options?.onUpdate as never,
			options?.callerContext ?? resolved.makeContext(),
		)) as AgentToolResult<TDetails>;
	}

	constructor(
		private readonly extensions: Extension[],
		private readonly runtime: ExtensionRuntime,
		/** Ignored: `cwd` is always read live via the `cwd` getter below, not cached here. */
		_initialCwd: string,
		private readonly sessionManager: SessionManager,
		private readonly modelRegistry: ModelRegistry,

View on GitHub (pinned to 9690622007)

Solutions

  1. Break the recursion: in your handler, invoke the underlying tool directly (bypass invokeTool) or skip interception when options.depth > 0.
  2. Pass/propagate depth correctly when re-invoking so the counter increments, and add an explicit depth guard in your handler.
  3. Restructure so the intercepted path terminates (invoke the resolved built-in once, not through the dispatcher).

Example fix

// before
api.onToolCall('bash', async evt => {
  return runner.invokeTool('bash', evt.params); // re-enters dispatcher forever
});
// after
api.onToolCall('bash', async evt => {
  if ((evt.depth ?? 0) > 0) return; // let inner call pass through
  return runner.invokeTool('bash', evt.params, { depth: 1 });
});
Defensive patterns

Strategy: try-catch

Validate before calling

if ((options?.depth ?? 0) >= 8) {
  throw new Error('refusing to delegate: depth limit would be exceeded');
}
await runner.invokeTool(name, params, { depth: (options?.depth ?? 0) + 1 });

Type guard

function canDelegate(depth: number | undefined): boolean {
  return (depth ?? 0) < 8;
}

Try / catch

try {
  return await runner.invokeTool(name, params, { depth });
} catch (err) {
  if (err.message.includes('delegation depth exceeded 8')) {
    logger.error('recursive invokeTool detected', { name, depth });
    throw new Error(`circular tool delegation involving '${name}'`);
  }
  throw err;
}

Prevention

When it happens

Trigger: An extension's invokeTool handler re-invokes the same tool, or a cycle of tools (A invokes B invokes A) nests more than 8 deep because options.depth is propagated.

Common situations: Extension wrapper intercepts a tool and calls invokeTool for the same tool name without a base-case condition; mutually recursive tool wrappers; default depth not threaded through a custom delegation chain.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/272e09b10cf892e5. Report an issue: GitHub.