import type { ProviderOptions } from '@ai-sdk/provider-utils';
import { z } from 'zod';

import type { Eval } from './eval';
import type { McpClient } from './mcp-client';
import { Memory, normalizeMemoryConfig, resolveMemoryConfigDefaults } from './memory';
import { Telemetry } from './telemetry';
import { Tool, wrapToolForApproval } from './tool';
import { AgentRuntime } from '../runtime/agent-runtime';
import { LOAD_TOOL_TOOL_NAME, SEARCH_TOOLS_TOOL_NAME } from '../runtime/deferred-tool-manager';
import { AgentEventBus } from '../runtime/event-bus';
import { createAgentToolResult } from '../runtime/tool-adapter';
import {
	appendSkillCatalogToInstructions,
	createRuntimeSkillSource,
	createRuntimeSkillTools,
	RUNTIME_SKILL_TOOL_NAMES,
} from '../skills';
import type { RuntimeSkill, RuntimeSkillSource } from '../skills';
import type {
	AgentEventHandler,
	AgentMiddleware,
	BuiltAgent,
	BuiltEval,
	BuiltGuardrail,
	BuiltMemory,
	BuiltProviderTool,
	BuiltTool,
	BuiltTelemetry,
	CheckpointStore,
	ExecutionOptions,
	GenerateResult,
	MemoryConfig,
	ModelConfig,
	Provider,
	RunOptions,
	SerializableAgentState,
	StreamResult,
	SubAgentUsage,
	ThinkingConfig,
	ThinkingConfigFor,
	ResumeOptions,
} from '../types';
import type { AgentEvent } from '../types/runtime/event';
import type { AgentBuilder } from '../types/sdk/agent-builder';
import type { AgentMessage } from '../types/sdk/message';
import type { Workspace } from '../workspace/workspace';

const DEFAULT_LAST_MESSAGES = 10;

type ToolParameter = BuiltTool | { build(): BuiltTool };

interface DeferredToolOptions {
	search?: {
		topK?: number;
	};
}

/**
 * Lightweight read-only view of an agent's configured state.
 * Returned by `Agent.snapshot` for testing and debugging purposes.
 */
export interface AgentSnapshot {
	/** Agent name. */
	name: string;
	/** Parsed model identifier. Both fields are null if no model has been set. */
	model: { provider: string | null; name: string | null };
	/** Instruction text passed to `.instructions()`, or null if not set. */
	instructions: string | null;
	/** Minimal description of each directly registered tool. */
	tools: ReadonlyArray<{ name: string; description: string | undefined }>;
	/** True when `.memory()` has been configured. */
	hasMemory: boolean;
	/** True when observation-log memory has been configured on the memory builder. */
	hasObservationalMemory: boolean;
	/** True when episodic memory has been configured on the memory builder. */
	hasEpisodicMemory: boolean;
	/** The thinking config if set, otherwise null. */
	thinking: ThinkingConfig | null;
	/** Tool-call concurrency limit if set, otherwise null. */
	toolCallConcurrency: number | null;
	/** Whether `.requireToolApproval()` was called. */
	requireToolApproval: boolean;
}

/**
 * Builder for creating AI agents with a fluent API.
 *
 * Usage:
 * ```typescript
 * const agent = new Agent('assistant')
 *   .model('anthropic', 'claude-sonnet-4')
 *   .instructions('You are a helpful assistant.')
 *   .tool(searchTool);
 *
 * const result = await agent.generate('Hello!');
 * ```
 */

export class Agent implements BuiltAgent, AgentBuilder {
	readonly name: string;

	private modelConfig?: ModelConfig;

	private instructionProviderOpts?: ProviderOptions;

	private instructionsText?: string;

	private tools: BuiltTool[] = [];

	private deferredTools: BuiltTool[] = [];

	private deferredToolSearchTopK: number | undefined;

	private providerTools: BuiltProviderTool[] = [];

	private skillSource?: RuntimeSkillSource;

	private hasRuntimeSkillTool = false;

	private memoryConfig?: MemoryConfig;

	// TODO: Guardrails are accepted by the builder API for forward
	// compatibility but not yet wired to the runtime.
	private inputGuardrails: BuiltGuardrail[] = [];

	private outputGuardrails: BuiltGuardrail[] = [];

	private agentEvals: BuiltEval[] = [];

	private outputSchema?: z.ZodType;

	private checkpointStore?: 'memory' | CheckpointStore;

	private thinkingConfig?: ThinkingConfig;

	private runtime?: AgentRuntime;

	private concurrencyValue?: number;

	private telemetryBuilder?: Telemetry;

	private telemetryConfig?: BuiltTelemetry;

	private middlewares: AgentMiddleware[] = [];

	private requireToolApprovalValue = false;

	private mcpClients: McpClient[] = [];

	private defaultExecutionOptions?: ExecutionOptions;

	private buildPromise: Promise<AgentRuntime> | undefined;

	private eventBus = new AgentEventBus();

	private workspaceInstance?: Workspace;

	constructor(name: string) {
		this.name = name;
	}

	hasCheckpointStorage(): boolean {
		return this.checkpointStore !== undefined;
	}

	hasMemory(): boolean {
		return this.memoryConfig !== undefined;
	}

	/**
	 * Set the model with provider type information.
	 *
	 * @example
	 * ```typescript
	 * // Typed form — enables provider-specific config on .thinking() etc.
	 * agent.model('anthropic', 'claude-sonnet-4-5')
	 *
	 * // Untyped form — backwards compatible
	 * agent.model('anthropic/claude-sonnet-4-5')
	 * ```
	 */
	model(providerOrIdOrConfig: string | ModelConfig, modelName?: string): this {
		if (typeof providerOrIdOrConfig === 'string') {
			this.modelConfig = modelName ? `${providerOrIdOrConfig}/${modelName}` : providerOrIdOrConfig;
		} else {
			this.modelConfig = providerOrIdOrConfig;
		}
		return this;
	}

	/** Set the system instructions for the agent. Required before building. */
	instructions(text: string, options?: { providerOptions?: ProviderOptions }): this {
		this.instructionsText = text;
		this.instructionProviderOpts = options?.providerOptions;
		return this;
	}

	/** Add a tool to the agent's capabilities. Accepts a built tool or a Tool builder (which will be built automatically). Can also accept an array of tools. */
	tool(t: ToolParameter | ToolParameter[]): this {
		const tools = Array.isArray(t) ? t : [t];
		const builtTools = tools.map((tool) => ('build' in tool ? tool.build() : tool));
		for (const built of builtTools) {
			this.assertToolNameAvailable(built.name);
		}
		this.tools.push(...builtTools);
		return this;
	}

	/** Add tools that are searchable through `search_tools` and activated on demand with `load_tool`. */
	deferredTool(t: ToolParameter | ToolParameter[], options?: DeferredToolOptions): this {
		const tools = Array.isArray(t) ? t : [t];
		for (const tool of tools) {
			const built = 'build' in tool ? tool.build() : tool;
			this.deferredTools.push(built);
		}
		if (options?.search?.topK !== undefined) {
			this.deferredToolSearchTopK = options.search.topK;
		}
		return this;
	}

	/**
	 * Add runtime-loadable skills to the agent. The model sees only a compact
	 * name/description catalog in the system prompt, then calls `load_skill`
	 * to retrieve the full instructions for a relevant skill.
	 */
	skills(sourceOrSkills: RuntimeSkillSource | RuntimeSkill[]): this {
		const source = Array.isArray(sourceOrSkills)
			? createRuntimeSkillSource(sourceOrSkills)
			: sourceOrSkills;

		this.removeRuntimeSkillTools();
		this.skillSource = source;
		if (source.registry.skills.length === 0) return this;

		const reservedTool = this.tools.find((tool) => RUNTIME_SKILL_TOOL_NAMES.has(tool.name));
		if (reservedTool) {
			throw new Error(`Tool name "${reservedTool.name}" is reserved for runtime skills`);
		}

		this.tools.push(...createRuntimeSkillTools(source));
		this.hasRuntimeSkillTool = true;
		return this;
	}

	/** Add a provider-defined tool (e.g. Anthropic web search, OpenAI code interpreter). */
	providerTool(builtProviderTool: BuiltProviderTool): this {
		this.providerTools.push(builtProviderTool);
		return this;
	}

	/** Read the declared tools. Lists only tools added via tool() */
	get declaredTools(): BuiltTool[] {
		return this.tools;
	}

	/** Set the memory configuration. Accepts a MemoryConfig, Memory builder, or bare BuiltMemory. */
	memory(m: MemoryConfig | Memory | BuiltMemory): this {
		if (m instanceof Memory) {
			// Memory builder — call build()
			this.memoryConfig = m.build();
		} else if ('memory' in m && 'lastMessages' in m) {
			// MemoryConfig — validate the same invariants as the builder path
			this.memoryConfig = normalizeMemoryConfig(m);
		} else if (
			typeof m === 'object' &&
			m !== null &&
			typeof m.getMessages === 'function' &&
			typeof m.saveMessages === 'function'
		) {
			// Bare BuiltMemory — wrap in minimal config
			this.memoryConfig = { memory: m, lastMessages: DEFAULT_LAST_MESSAGES };
		} else {
			throw new Error(
				'Invalid memory configuration. Use: new Memory().lastMessages(N) for in-process memory, ' +
					'or new Memory().storage(myBuiltMemoryBackend).lastMessages(N) for a persistent backend. ' +
					'See the Memory class documentation for all options.',
			);
		}
		return this;
	}

	/** Add a middleware. */
	middleware(m: AgentMiddleware): this {
		this.middlewares.push(m);
		return this;
	}

	// TODO: guardrails can be a middleware internally
	/** Add an input guardrail. Accepts a built guardrail or a Guardrail builder. */
	inputGuardrail(g: BuiltGuardrail | { build(): BuiltGuardrail }): this {
		this.inputGuardrails.push('_config' in g ? g : g.build());
		return this;
	}

	/** Add an output guardrail. Accepts a built guardrail or a Guardrail builder. */
	outputGuardrail(g: BuiltGuardrail | { build(): BuiltGuardrail }): this {
		this.outputGuardrails.push('_config' in g ? g : g.build());
		return this;
	}

	/** Add an eval to run after each agent response. Accepts an Eval builder or BuiltEval. */
	eval(e: Eval | BuiltEval | { ensureBuilt(): BuiltEval }): this {
		const built = '_run' in e ? e : (e as Eval).ensureBuilt();
		this.agentEvals.push(built);
		return this;
	}

	/**
	 * Set the checkpoint storage for tool suspend/resume (human-in-the-loop).
	 * Required when any tool uses `.suspend()` / `.resume()`.
	 *
	 * - `'memory'` — in-process storage (lost on restart, fine for dev)
	 * - A storage provider instance (e.g. `new LibSQLStore(...)`, `new PgStore(...)`)
	 *
	 * @example
	 * ```typescript
	 * const agent = new Agent('assistant')
	 *   .model('anthropic/claude-sonnet-4-5')
	 *   .instructions('...')
	 *   .tool(dangerousTool) // has .suspend() / .resume()
	 *   .checkpoint('memory');
	 * ```
	 */
	checkpoint(storage: 'memory' | CheckpointStore): this {
		this.checkpointStore = storage;
		return this;
	}

	/**
	 * Set a structured output schema. When set, the agent's response will be
	 * parsed into a typed object matching the schema, available as `result.output`.
	 *
	 * @example
	 * ```typescript
	 * const agent = new Agent('extractor')
	 *   .model('anthropic/claude-sonnet-4-5')
	 *   .instructions('Extract structured data.')
	 *   .structuredOutput(z.object({
	 *     code: z.string(),
	 *     explanation: z.string(),
	 *   }));
	 *
	 * const result = await agent.generate('...');
	 * console.log(result.structuredOutput); // { code: '...', explanation: '...' }
	 * ```
	 */
	structuredOutput(schema: z.ZodType): this {
		this.outputSchema = schema;
		return this;
	}

	/**
	 * Enable extended thinking / reasoning for the agent.
	 * The config type is inferred from the provider set via `.model()`.
	 *
	 * @example
	 * ```typescript
	 * // Anthropic — budgetTokens
	 * new Agent('thinker')
	 *   .model('anthropic', 'claude-sonnet-4-5')
	 *   .thinking('anthropic', { budgetTokens: 5000 })
	 *
	 * // OpenAI — reasoningEffort
	 * new Agent('thinker')
	 *   .model('openai', 'o3-mini')
	 *   .thinking('openai', { reasoningEffort: 'high' })
	 * ```
	 */
	thinking<P extends Provider>(_provider: P, config?: ThinkingConfigFor<P>): this {
		this.thinkingConfig = config ?? {};
		return this;
	}

	/** Set telemetry configuration for this agent. Accepts a Telemetry builder or pre-built config. */
	telemetry(t: Telemetry | BuiltTelemetry): this {
		if (t instanceof Telemetry) {
			this.telemetryBuilder = t;
			this.telemetryConfig = undefined;
		} else {
			this.telemetryBuilder = undefined;
			this.telemetryConfig = t;
			this.runtime?.setTelemetry(t);
		}
		return this;
	}

	/** @internal Read the declared telemetry builder (used by the execution engine to resolve credentials). */
	protected get declaredTelemetry(): Telemetry | undefined {
		return this.telemetryBuilder;
	}

	/**
	 * Set the number of tool calls to execute concurrently within a single LLM turn.
	 *
	 * - `1` (default) — sequential execution, fully backward-compatible.
	 * - `Infinity` — unlimited parallelism (all tool calls start at once).
	 * - Any number in between — bounded concurrency (e.g. `5` = at most 5 tools run simultaneously).
	 */
	toolCallConcurrency(n: number): this {
		if ((n !== Infinity && !Number.isInteger(n)) || n < 1) {
			throw new Error('toolCallConcurrency must be a positive integer or Infinity');
		}
		this.concurrencyValue = n;
		return this;
	}

	/**
	 * Require human approval before any tool executes.
	 * Tools that already have .suspend()/.resume() (suspendSchema) are skipped.
	 * Requires .checkpoint() to be set.
	 */
	requireToolApproval(): this {
		this.requireToolApprovalValue = true;
		return this;
	}

	/**
	 * Attach a workspace to this agent. Workspace tools and instructions
	 * are injected at build time.
	 */
	workspace(ws: Workspace): this {
		this.workspaceInstance = ws;
		return this;
	}

	/**
	 * Add an MCP client as a tool source for this agent.
	 * Tools from all servers in the client become available to the agent.
	 * Multiple clients can be added; tools are merged across all of them.
	 *
	 * @example
	 * ```typescript
	 * const client = new McpClient([
	 *   { name: 'browser', url: 'http://localhost:9222/mcp', transport: 'streamableHttp' },
	 *   { name: 'fs', command: 'npx', args: ['@anthropic/mcp-fs', '/tmp'] },
	 * ]);
	 *
	 * const agent = new Agent('assistant')
	 *   .model('anthropic', 'claude-sonnet-4')
	 *   .mcp(client)
	 *   .instructions('You are a helpful assistant.');
	 * ```
	 */
	mcp(client: McpClient): this {
		this.mcpClients.push(client);
		return this;
	}

	/**
	 * Set default execution options for all `generate()` and `stream()` calls.
	 * Options passed directly to those methods take precedence over these defaults.
	 *
	 * @example
	 * ```typescript
	 * const agent = new Agent('assistant')
	 *   .model('anthropic/claude-sonnet-4-5')
	 *   .instructions('You are a helpful assistant.')
	 *   .configuration({ maxIterations: 5 });
	 *
	 * // Uses maxIterations: 5 from defaults
	 * await agent.generate('Hello');
	 *
	 * // Overrides maxIterations to 10 for this call only
	 * await agent.generate('Hello', { maxIterations: 10 });
	 * ```
	 */
	configuration(options: ExecutionOptions): this {
		this.defaultExecutionOptions = options;
		return this;
	}

	/** Get the evals attached to this agent. */
	get evaluations(): BuiltEval[] {
		return [...this.agentEvals];
	}

	/**
	 * Register a handler for an agent lifecycle event.
	 * Handlers are called synchronously during the agentic loop.
	 */
	on(event: AgentEvent, handler: AgentEventHandler): void {
		this.eventBus.on(event, handler);
	}

	/**
	 * Remove a previously registered event handler. Pair with `on()` so
	 * per-request subscribers (e.g. the cli's ExecutionRecorder) can detach
	 * cleanly between turns instead of accumulating on a long-lived agent.
	 */
	off(event: AgentEvent, handler: AgentEventHandler): void {
		this.eventBus.off(event, handler);
	}

	/**
	 * Wrap this agent as a tool for use in multi-agent composition.
	 * The tool sends a text prompt to this agent and returns the text of the response.
	 *
	 * @example
	 * ```typescript
	 * const coordinatorAgent = new Agent('coordinator')
	 *   .model('anthropic/claude-sonnet-4-5')
	 *   .instructions('Route tasks to specialist agents.')
	 *   .tool(writerAgent.asTool('Write content given a topic'));
	 * ```
	 */
	asTool(description: string): BuiltTool {
		// eslint-disable-next-line @typescript-eslint/no-this-alias
		const agent = this;

		const tool = new Tool(this.name)
			.description(description)
			.input(
				z.object({
					input: z.string().describe('The input to send to the agent'),
				}),
			)
			.output(
				z.object({
					result: z.string().describe('The result of the agent'),
				}),
			)
			.handler(async (rawInput, ctx) => {
				const { input } = rawInput as { input: string };
				const result = await agent.generate(input, {
					telemetry: ctx.parentTelemetry,
				} as RunOptions & ExecutionOptions);

				const text = result.messages
					.filter((m) => 'role' in m && m.role === 'assistant')
					.flatMap((m) => ('content' in m ? m.content : []))
					.filter((c) => c.type === 'text')
					.map((c) => ('text' in c ? c.text : ''))
					.join('');

				// Collect sub-agent usage: this agent's own + any nested sub-agents
				const subAgentUsage: SubAgentUsage[] = [];
				if (result.usage) {
					subAgentUsage.push({ agent: agent.name, model: result.model, usage: result.usage });
				}
				if (result.subAgentUsage) {
					subAgentUsage.push(...result.subAgentUsage);
				}

				// Return branded result — the runtime unwraps it to extract sub-agent usage.
				// createAgentToolResult returns `never`, same pattern as ctx.suspend().
				if (subAgentUsage.length > 0) {
					return createAgentToolResult({ result: text }, subAgentUsage);
				}
				return { result: text };
			});

		return tool.build();
	}

	/**
	 * Return a lightweight read-only snapshot of the agent's configured state.
	 * Useful for testing and debugging — does not trigger a build.
	 */
	get snapshot(): AgentSnapshot {
		let model: AgentSnapshot['model'];
		const rawModelId =
			typeof this.modelConfig === 'string'
				? this.modelConfig
				: this.modelConfig && typeof this.modelConfig === 'object' && 'id' in this.modelConfig
					? this.modelConfig.id
					: undefined;

		if (rawModelId) {
			const slashIdx = rawModelId.indexOf('/');
			if (slashIdx === -1) {
				model = { provider: null, name: rawModelId };
			} else {
				model = {
					provider: rawModelId.slice(0, slashIdx),
					name: rawModelId.slice(slashIdx + 1),
				};
			}
		} else {
			model = { provider: null, name: null };
		}

		return {
			name: this.name,
			model,
			instructions: this.instructionsText ?? null,
			tools: this.tools.map((t) => ({ name: t.name, description: t.description })),
			hasMemory: this.memoryConfig !== undefined,
			hasObservationalMemory: this.memoryConfig?.observationalMemory !== undefined,
			hasEpisodicMemory: this.memoryConfig?.episodicMemory !== undefined,
			thinking: this.thinkingConfig ?? null,
			toolCallConcurrency: this.concurrencyValue ?? null,
			requireToolApproval: this.requireToolApprovalValue,
		};
	}

	/** Return the latest state snapshot of the agent. Returns `{ status: 'idle' }` before first run. */
	getState(): SerializableAgentState {
		if (!this.runtime) {
			return {
				persistence: undefined,
				status: 'idle',
				messageList: { messages: [], historyIds: [], inputIds: [], responseIds: [] },
				pendingToolCalls: {},
			};
		}
		return this.runtime.getState();
	}

	/**
	 * Cancel the currently running agent.
	 * Synchronous — sets an abort flag; the agentic loop checks it asynchronously.
	 */
	abort(): void {
		this.eventBus.abort();
	}

	/**
	 * Close the agent and release all held resources.
	 *
	 * - Waits for any in-flight background tasks (title generation, observer
	 *   cycles) to settle via the runtime's `dispose()`.
	 * - Disconnects every MCP client attached via `.mcp()`. Errors from
	 *   individual client disconnects are swallowed so a single misbehaving
	 *   server does not prevent the others from closing.
	 *
	 * Safe to call multiple times.
	 */
	async close(): Promise<void> {
		const tasks: Array<Promise<unknown>> = [];
		if (this.runtime) tasks.push(this.runtime.dispose());
		tasks.push(...this.mcpClients.map(async (c) => await c.close()));
		await Promise.allSettled(tasks);
	}

	/** Generate a response (non-streaming). Lazy-builds on first call. */
	async generate(
		input: AgentMessage[] | string,
		options?: RunOptions & ExecutionOptions,
	): Promise<GenerateResult> {
		const runtime = await this.ensureBuilt();
		const mergedOptions = this.mergeWithDefaults(options);
		return await runtime.generate(this.toMessages(input), mergedOptions);
	}

	/** Stream a response. Lazy-builds on first call. */
	async stream(
		input: AgentMessage[] | string,
		options?: RunOptions & ExecutionOptions,
	): Promise<StreamResult> {
		const runtime = await this.ensureBuilt();
		const mergedOptions = this.mergeWithDefaults(options);
		return await runtime.stream(this.toMessages(input), mergedOptions);
	}

	/** Resume a suspended tool call with data. Lazy-builds on first call. */
	async resume(
		method: 'generate',
		data: unknown,
		options: ResumeOptions & ExecutionOptions,
	): Promise<GenerateResult>;
	async resume(
		method: 'stream',
		data: unknown,
		options: ResumeOptions & ExecutionOptions,
	): Promise<StreamResult>;
	async resume(
		method: 'generate' | 'stream',
		data: unknown,
		options: ResumeOptions & ExecutionOptions,
	): Promise<GenerateResult | StreamResult> {
		const runtime = await this.ensureBuilt();
		if (method === 'generate') {
			return await runtime.resume('generate', data, options);
		}
		return await runtime.resume('stream', data, options);
	}

	approve(method: 'generate', options: ResumeOptions & ExecutionOptions): Promise<GenerateResult>;
	approve(method: 'stream', options: ResumeOptions & ExecutionOptions): Promise<StreamResult>;
	async approve(
		method: 'generate' | 'stream',
		options: ResumeOptions & ExecutionOptions,
	): Promise<GenerateResult | StreamResult> {
		if (method === 'generate') {
			return await this.resume('generate', { approved: true }, options);
		}
		return await this.resume('stream', { approved: true }, options);
	}

	deny(method: 'generate', options: ResumeOptions & ExecutionOptions): Promise<GenerateResult>;
	deny(method: 'stream', options: ResumeOptions & ExecutionOptions): Promise<StreamResult>;
	async deny(
		method: 'generate' | 'stream',
		options: ResumeOptions & ExecutionOptions,
	): Promise<GenerateResult | StreamResult> {
		if (method === 'generate') {
			return await this.resume('generate', { approved: false }, options);
		}
		return await this.resume('stream', { approved: false }, options);
	}

	private mergeWithDefaults(
		options?: RunOptions & ExecutionOptions,
	): (RunOptions & ExecutionOptions) | undefined {
		if (!this.defaultExecutionOptions) return options;
		return { ...this.defaultExecutionOptions, ...options };
	}

	/**
	 * @internal Lazy-build the agent on first use. Stores the promise so
	 * concurrent callers share one build operation. On error the promise is
	 * cleared so the caller can retry.
	 */
	private async ensureBuilt(): Promise<AgentRuntime> {
		if (!this.buildPromise) {
			const p = this.build();
			this.buildPromise = p;
			p.catch(() => {
				if (this.buildPromise === p) this.buildPromise = undefined;
			});
		}
		return await this.buildPromise;
	}

	private toMessages(input: string | AgentMessage[]): AgentMessage[] {
		if (Array.isArray(input)) return input;
		return [{ role: 'user', content: [{ type: 'text', text: input }] }];
	}

	/** @internal Validate configuration and produce an AgentRuntime. Overridden by the execution engine. */
	protected async build(): Promise<AgentRuntime> {
		if (!this.modelConfig) {
			throw new Error(`Agent "${this.name}" requires a model`);
		}
		if (!this.instructionsText) {
			throw new Error(`Agent "${this.name}" requires instructions`);
		}

		const finalTools = [...this.tools];
		const configuredDeferredTools = [...this.deferredTools];

		if (this.workspaceInstance) {
			const wsTools = this.workspaceInstance.getTools();
			finalTools.push(...wsTools);
		}

		let finalStaticTools = finalTools;
		let finalDeferredTools = configuredDeferredTools;
		if (this.requireToolApprovalValue) {
			finalStaticTools = finalTools.map((t) =>
				RUNTIME_SKILL_TOOL_NAMES.has(t.name) || t.suspendSchema
					? t
					: wrapToolForApproval(t, { requireApproval: true }),
			);
			finalDeferredTools = configuredDeferredTools.map((t) =>
				t.suspendSchema ? t : wrapToolForApproval(t, { requireApproval: true }),
			);
		}

		// Validate checkpoint requirement from static tools and known MCP approval config
		// before attempting any network connections (allows fast failure).
		const staticNeedsCheckpoint =
			finalStaticTools.some((t) => t.suspendSchema) ||
			finalDeferredTools.some((t) => t.suspendSchema);
		const mcpNeedsCheckpoint =
			(this.requireToolApprovalValue && this.mcpClients.length > 0) ||
			this.mcpClients.some((c) => c.declaresApproval());
		if ((staticNeedsCheckpoint || mcpNeedsCheckpoint) && !this.checkpointStore) {
			throw new Error(
				`Agent "${this.name}" has tools requiring approval or suspend/resume but no checkpoint storage. ` +
					"Add .checkpoint('memory') for in-process storage, " +
					'or pass a persistent store (e.g. LibSQLStore, PgStore).',
			);
		}

		// Resolve tools from all MCP clients.
		const mcpToolLists = await Promise.all(this.mcpClients.map(async (c) => await c.listTools()));
		let mcpTools = mcpToolLists.flat();

		// Apply global requireToolApproval to MCP tools (per-server approval is already
		// handled inside McpClient/McpConnection.listTools()).
		if (this.requireToolApprovalValue) {
			mcpTools = mcpTools.map((t) =>
				t.suspendSchema ? t : wrapToolForApproval(t, { requireApproval: true }),
			);
		}

		// Detect collisions between direct, deferred, and MCP tools.
		const staticCollisions = findDuplicateToolNames(finalStaticTools);
		if (staticCollisions.length > 0) {
			throw new Error(
				`Static tool name collision — the following tool names resolve to duplicates: ${staticCollisions.join(', ')}`,
			);
		}

		const staticNames = new Set(finalStaticTools.map((t) => t.name));
		const reservedDeferredToolNames = new Set([
			SEARCH_TOOLS_TOOL_NAME,
			LOAD_TOOL_TOOL_NAME,
			...RUNTIME_SKILL_TOOL_NAMES,
		]);
		const deferredNames = new Set<string>();
		const deferredCollisions: string[] = [];
		for (const tool of finalDeferredTools) {
			if (
				staticNames.has(tool.name) ||
				reservedDeferredToolNames.has(tool.name) ||
				deferredNames.has(tool.name)
			) {
				deferredCollisions.push(tool.name);
			}
			deferredNames.add(tool.name);
		}
		if (deferredCollisions.length > 0) {
			throw new Error(
				`Deferred tool name collision — the following tool names resolve to duplicates or reserved tools: ${deferredCollisions.join(', ')}`,
			);
		}

		const collisions = mcpTools
			.filter((t) => staticNames.has(t.name) || deferredNames.has(t.name))
			.map((t) => t.name);
		if (collisions.length > 0) {
			throw new Error(
				`MCP tool name collision — the following tool names resolve to duplicates: ${collisions.join(', ')}`,
			);
		}

		const allTools = [...finalStaticTools, ...mcpTools];

		// Validate checkpoint again after discovering actual MCP tools
		// (catches the case where MCP tools have suspendSchema after listing).
		const allNeedCheckpoint =
			allTools.some((t) => t.suspendSchema) || finalDeferredTools.some((t) => t.suspendSchema);
		if (allNeedCheckpoint && !this.checkpointStore) {
			throw new Error(
				`Agent "${this.name}" has tools requiring approval or suspend/resume but no checkpoint storage. ` +
					"Add .checkpoint('memory') for in-process storage, " +
					'or pass a persistent store (e.g. LibSQLStore, PgStore).',
			);
		}

		const modelConfig: ModelConfig = this.modelConfig;
		const memoryConfig = this.memoryConfig
			? resolveMemoryConfigDefaults(this.memoryConfig, { defaultModel: modelConfig })
			: undefined;

		let instructions = this.instructionsText;
		if (this.skillSource) {
			await this.skillSource.prepare?.();
			instructions = appendSkillCatalogToInstructions(instructions, this.skillSource.registry);
		}
		if (this.workspaceInstance) {
			const wsInstructions = this.workspaceInstance.getInstructions();
			if (wsInstructions) {
				instructions = `${instructions}\n\n${wsInstructions}`;
			}
		}

		this.runtime = new AgentRuntime({
			name: this.name,
			model: modelConfig,
			instructions,
			tools: allTools.length > 0 ? allTools : undefined,
			deferredTools: finalDeferredTools.length > 0 ? finalDeferredTools : undefined,
			toolSearch:
				finalDeferredTools.length > 0 && this.deferredToolSearchTopK !== undefined
					? { topK: this.deferredToolSearchTopK }
					: undefined,
			instructionProviderOptions: this.instructionProviderOpts,
			providerTools: this.providerTools.length > 0 ? this.providerTools : undefined,
			memory: memoryConfig?.memory,
			lastMessages: memoryConfig?.lastMessages,
			observationLog: memoryConfig?.observationLog,
			observationalMemory: memoryConfig?.observationalMemory,
			episodicMemory: memoryConfig?.episodicMemory,
			semanticRecall: memoryConfig?.semanticRecall,
			structuredOutput: this.outputSchema,
			checkpointStorage: this.checkpointStore,
			thinking: this.thinkingConfig,
			eventBus: this.eventBus,
			toolCallConcurrency: this.concurrencyValue,
			titleGeneration: memoryConfig?.titleGeneration,
			telemetry: this.telemetryConfig ?? (await this.telemetryBuilder?.build()),
		});

		return this.runtime;
	}

	private assertToolNameAvailable(toolName: string): void {
		if (!this.hasRuntimeSkillTool || !RUNTIME_SKILL_TOOL_NAMES.has(toolName)) return;

		throw new Error(`Tool name "${toolName}" is reserved for runtime skills`);
	}

	private removeRuntimeSkillTools(): void {
		if (!this.hasRuntimeSkillTool) return;

		this.tools = this.tools.filter((tool) => !RUNTIME_SKILL_TOOL_NAMES.has(tool.name));
		this.hasRuntimeSkillTool = false;
	}
}

function findDuplicateToolNames(tools: BuiltTool[]): string[] {
	const seen = new Set<string>();
	const duplicates = new Set<string>();
	for (const tool of tools) {
		if (seen.has(tool.name)) {
			duplicates.add(tool.name);
		}
		seen.add(tool.name);
	}
	return [...duplicates].sort();
}
