· Plugin API

Plugin API 参考

请参阅 插件指南 ,了解设置、能力和完整示例。本页包含一个端到端插件以及生成的 @ampcode/plugin 类型参考。

示例插件:Kitchen Sink

这个单一插件演示了所有插件能力:事件、命令、工具、UI 和 AI 辅助函数。将其保存为 .amp/plugins/kitchen-sink.ts,然后运行 plugins: reload 命令面板中的命令。

import type { PluginAPI } from '@ampcode/plugin'

const marker = '[kitchen-sink]'

export default function (amp: PluginAPI) {
	amp.logger.log(`${marker} plugin initialized`)

	amp.on('session.start', async (event, ctx) => {
		await ctx.ui.notify(`Kitchen sink session.start for ${event.thread.id}.`)
	})

	amp.on('tool.call', async (event, ctx) => {
		ctx.logger.log(`tool.call: ${event.tool}`)
		const shellCommand = amp.helpers.shellCommandFromToolCall(event)
		const files = amp.helpers.filesModifiedByToolCall(event)
		ctx.logger.log(
			`helper summary: shell=${shellCommand?.command ?? 'none'} files=${
				files?.map((file) => amp.helpers.filePathFromURI(file)).join(', ') ?? 'none'
			}`,
		)

		if (event.tool === 'kitchen_sink_tool') {
			return { action: 'allow' }
		}

		const confirmed = await ctx.ui.confirm({
			title: `Allow ${event.tool}?`,
			message: 'Kitchen sink observed a tool call.',
			confirmButtonText: 'Allow',
		})

		if (confirmed) {
			return { action: 'allow' }
		}

		return {
			action: 'reject-and-continue',
			message: `Kitchen sink rejected ${event.tool}.`,
		}
	})

	amp.on('tool.result', async (event, ctx) => {
		ctx.logger.log(`tool.result: ${event.tool} ${event.status}`)
		if (event.status === 'error') {
			await ctx.ui.notify(`Kitchen sink saw ${event.tool} fail.`)
		}
	})

	amp.on('agent.start', async (event, ctx) => {
		if (!event.message.toLowerCase().includes('kitchen sink')) {
			return
		}

		const answer = await amp.ai.ask(`Is this a kitchen sink request? ${event.message}`)
		await ctx.ui.notify(`AI helper answered: ${answer.result}`)

		return {
			message: {
				content: `${marker} agent.start hook received this turn.`,
				display: true,
			},
		}
	})

	amp.on('agent.end', (event) => {
		const toolCalls = amp.helpers.toolCallsInMessages(event.messages)
		amp.logger.log(`agent.end saw ${toolCalls.length} completed tool calls`)

		if (!event.message.toLowerCase().includes('kitchen sink continue')) {
			return
		}
		if (event.message.includes(`${marker} continued`)) {
			return
		}

		return {
			action: 'continue',
			userMessage: `${marker} continued. Reply with exactly KITCHEN_SINK_CONTINUED.`,
		}
	})

	amp.registerCommand(
		'show-kitchen-sink-notification',
		{
			title: 'Show kitchen sink notification',
			category: 'kitchen-sink',
			description: 'Show a notification from the kitchen sink plugin.',
		},
		async (ctx) => {
			await ctx.ui.notify('Kitchen sink command ran.')
		},
	)

	amp.registerCommand(
		'run-kitchen-sink-ui',
		{
			title: 'Run kitchen sink UI',
			category: 'kitchen-sink',
			description: 'Run notify, input, select, and confirm dialogs in sequence.',
		},
		async (ctx) => {
			await ctx.ui.notify('Starting the kitchen sink UI sequence.')
			const note = await ctx.ui.input({
				title: 'Kitchen sink input',
				helpText: 'Enter a note to append to the current thread.',
				initialValue: 'Hello from the kitchen sink plugin.',
				submitButtonText: 'Continue',
			})

			const choice = await ctx.ui.select({
				title: 'Kitchen sink select',
				message: 'Choose what to do with the note.',
				options: ['Append to thread', 'Show notification only', 'Cancel'],
			})

			const confirmed = await ctx.ui.confirm({
				title: 'Finish kitchen sink UI?',
				message: `Input: ${note ?? '(cancelled)'}\nChoice: ${choice ?? '(cancelled)'}`,
				confirmButtonText: 'Finish',
			})

			if (confirmed && choice === 'Append to thread' && note) {
				if (!ctx.thread) {
					await ctx.ui.notify('No active thread. Send any message to create one, then re-run this command.')
					return
				}

				await ctx.thread.append([{ type: 'user-message', content: note }])
			}

			await ctx.ui.notify(
				confirmed ? 'Kitchen sink UI finished.' : 'Kitchen sink UI cancelled.',
			)
		},
	)

	amp.registerCommand(
		'open-kitchen-sink-docs',
		{
			title: 'Open kitchen sink docs',
			category: 'kitchen-sink',
			description: 'Open the Plugin API reference page.',
		},
		async (ctx) => {
			await ctx.system.open('https://ampcode.com/manual/plugin-api')
		},
	)

	amp.registerCommand(
		'show-kitchen-sink-runtime',
		{
			title: 'Show kitchen sink runtime',
			category: 'kitchen-sink',
			description: 'Show configuration, shell, and system information.',
		},
		async (ctx) => {
			const config = await amp.configuration.get()
			const pwd = await amp.$`pwd`
			await ctx.ui.notify(
				[
					`Amp URL: ${amp.system.ampURL}`,
					`User: ${amp.system.user?.email ?? '(not authenticated)'}`,
					`Executor: ${amp.system.executor.kind}`,
					`Working directory: ${pwd.stdout.trim()}`,
					`Config keys: ${Object.keys(config).sort().join(', ') || '(none)'}`,
				].join('\n'),
			)
		},
	)

	amp.registerCommand(
		'append-kitchen-sink-message',
		{
			title: 'Append kitchen sink message',
			category: 'kitchen-sink',
			description: 'Append a user message to the active thread.',
		},
		async (ctx) => {
			if (!ctx.thread) {
				await ctx.ui.notify('No active thread. Send any message to create one, then re-run this command.')
				return
			}

			await ctx.thread.append([
				{ type: 'user-message', content: 'Message appended by kitchen sink plugin.' },
			])
		},
	)

	amp.registerTool({
		name: 'kitchen_sink_tool',
		description: 'Returns a short message proving the plugin tool ran.',
		inputSchema: {
			type: 'object',
			properties: {
				message: {
					type: 'string',
					description: 'Message to echo back.',
				},
			},
			required: ['message'],
		},
		async execute(input, ctx) {
			const message = typeof input.message === 'string' ? input.message : '(no message)'
			ctx.logger.log(`kitchen_sink_tool received: ${message}`)
			return `Kitchen sink tool received: ${message}`
		},
	})
}

可以用以下提示词和命令试用:

  1. 在命令面板中运行 kitchen-sink: show kitchen sink notification
  2. 运行 kitchen-sink: run kitchen sink UI 来测试通知、输入框、选择框、确认框和追加对话。
  3. 运行 kitchen-sink: open kitchen sink docs 来测试 ctx.system.open(...)
  4. 运行 kitchen-sink: show kitchen sink runtime 来测试配置、shell 执行和系统元数据。
  5. 在命令面板中运行 kitchen-sink: append kitchen sink message
  6. 询问 Amp: Use the kitchen_sink_tool with message hello.
  7. 询问 Amp: kitchen sink this turn.
  8. 询问 Amp: kitchen sink continue.

导出函数体演示了插件加载时的初始化。UI 命令演示了 ctx.ui.notifyctx.ui.inputctx.ui.selectctx.ui.confirmctx.thread?.append(...)。运行时命令演示了 amp.configuration.get()amp.$amp.system。工具提示词演示了已注册工具以及它周围的 tool.calltool.result 钩子。kitchen sink this turn 提示词演示了 agent.startamp.ai.ask。最后一个提示词演示了 agent.end,它会自动启动一个后续回合。

示例插件:自定义 Agent 模式

Use amp.createAgent(...) and amp.registerAgentMode(...) 可添加会与 Amp 内置模式一起出现在受支持客户端中的模式。外部插件必须为每个已注册模式包含一条匹配的 // @amp-agent-mode ... 元数据注释,包含模式的 key and label 。客户端用这些注释做静态发现,并在运行时注册与指令不匹配时显示警告 toast。一个插件文件中支持多条 mode 注释。 Save this as .amp/plugins/architect-mode.ts,然后运行 plugins: reload 命令面板中的命令。

// @amp-agent-mode {"key":"architect","label":"architect"}

import type { PluginAPI } from '@ampcode/plugin'

export default function (amp: PluginAPI) {
	const architect = amp.createAgent({
		name: 'architect',
		model: 'openai/gpt-5.5',
		instructions: [
			'You are an architecture-focused Amp mode.',
			'Before editing code, map the current design, name the tradeoffs,',
			'and prefer small changes that preserve clear module boundaries.',
		].join(' '),
		tools: 'all',
		reasoningEffort: 'high',
	})

	amp.registerAgentMode({
		key: 'architect',
		label: 'architect',
		description: 'Plan and implement changes with extra architecture scrutiny.',
		color: '#7c3aed',
		agent: architect.definition,
	})
}

自定义模式的 key 与 label 必须唯一、非空、不超过 16 个字符,且不得与内置模式冲突。已有的、未带指令却注册 agent 模式的外部插件仍可加载且模式仍可用,但客户端会警告,直到插件添加匹配的元数据并重新加载。

示例插件:跨线程消息

Use amp.threads.get(threadID) 可在插件需要特定线程句柄(而非当前调用线程)时使用。传入 { steer: true } 可在消息排在进行中工作之后时优先发送该追加消息。

import type { PluginAPI, ThreadID } from '@ampcode/plugin'

export default function (amp: PluginAPI) {
	amp.registerTool({
		name: 'send_to_thread',
		description: 'Append a user message to another Amp thread by thread ID.',
		inputSchema: {
			type: 'object',
			properties: {
				threadID: { type: 'string' },
				message: { type: 'string' },
			},
			required: ['threadID', 'message'],
		},
		async execute(input) {
			const threadID = typeof input.threadID === 'string' ? input.threadID : ''
			const message = typeof input.message === 'string' ? input.message : ''
			if (!threadID.startsWith(`'T-'`) || !message.trim()) {
				return 'Expected threadID and message.'
			}

			await amp.threads.get(threadID as ThreadID).appendUserMessage(
				{ type: 'user-message', content: message },
				{ steer: true },
			)
			return 'Sent message to ' + threadID + '.'
		},
	})
}

示例插件:自定义 Subagent

当你希望主 agent 按需将某类工作委托出去时,可创建 agent 并通过插件工具暴露它。 parentThreadID 选项会让 subagent 运行保持与调用该工具的线程相连。

import type { PluginAPI } from '@ampcode/plugin'

export default function (amp: PluginAPI) {
	const reviewer = amp.createAgent({
		name: 'focused-reviewer',
		model: 'openai/gpt-5.5',
		instructions: [
			'You are a focused code-review subagent.',
			'Inspect only the files and concerns named by the caller.',
			'Return concise findings with severity, evidence, and suggested fixes.',
		].join(' '),
		tools: 'all',
		reasoningEffort: 'medium',
	})

	amp.registerTool({
		name: 'focused_review_subagent',
		description: 'Run a focused code-review subagent for a specific review request.',
		inputSchema: {
			type: 'object',
			properties: {
				request: {
					type: 'string',
					description: 'The files, diff, or concern the subagent should review.',
				},
			},
			required: ['request'],
		},
		async execute(input, ctx) {
			const request = typeof input.request === 'string' ? input.request : ''
			if (!request.trim()) {
				return 'Missing review request.'
			}

			const result = await reviewer.run(request, {
				parentThreadID: ctx.thread.id,
				timeoutMs: 10 * 60 * 1000,
			})

			return result.text
		},
	})
}

@ampcode/plugin 类型参考

/**
 * # Amp Plugin API
 *
 * Plugins are JavaScript/TypeScript programs that extend & customize Amp.
 * They are long-lived processes that may run for multiple threads concurrently.
 *
 * Plugins live in `.amp/plugins/` (project) or `~/.config/amp/plugins/` (system) and are executed using Bun.
 *
 * A plugin exports a default function that receives a {@link PluginAPI} instance. For example:
 *
 * ```ts
 * import type { PluginAPI } from '@ampcode/plugin'
 *
 * export default function (amp: PluginAPI) {
 *   amp.logger.log('Plugin initialized')
 * }
 * ```
 */

/**
 * The plugin API object passed to the plugin's default export function.
 */
export interface PluginAPI {
	/** Logger scoped to this plugin */
	logger: PluginLogger

	/** System capabilities and information */
	system: PluginSystem

	/** Observable configuration that streams changes */
	configuration: PluginConfiguration<Record<string, unknown>>

	/**
	 * Execute shell commands using Bun's shell.
	 * Unlike `ctx.$` in event handlers, this is not tied to a specific hook invocation.
	 */
	$: ShellFunction

	/**
	 * Helper utilities for interpreting tool events.
	 */
	helpers: {
		shellCommandFromToolCall: ShellCommandFromToolCall
		toolCallsInMessages: ToolCallsInMessages
		filesModifiedByToolCall: FilesModifiedByToolCall
		filePathFromURI: FilePathFromURI
		isPluginUINotAvailableError: IsPluginUINotAvailableError
	}

	/** Platform UI capabilities */
	ui: PluginUI

	/**
	 * Register a handler for plugin events.
	 * For request events (e.g., tool.call), the handler must return a result.
	 * For fire-and-forget events, the handler returns void.
	 *
	 * If multiple plugins listen on the same event, the order in which each plugin's event handler is executed is not defined.
	 */
	on<E extends keyof PluginEventMap>(
		event: E,
		handler: (event: PluginEventMap[E], ctx: PluginEventContext<E>) => PluginHandlerResult<E>,
	): Subscription

	/**
	 * Register a command that appears in Amp's command palette.
	 * When the user invokes the command, the handler is called.
	 *
	 * @param id - Stable identifier for the command (e.g., "hello-world").
	 * @param options - Configuration for the command including title, category, and description.
	 * @param handler - The function to execute when the command is invoked.
	 *
	 * @example
	 * ```ts
	 * amp.registerCommand('hello-world', { title: 'greet', category: 'hello', description: 'Say hello' }, async (ctx) => {
	 *   await ctx.ui.notify('Hello, world!')
	 * })
	 * ```
	 */
	registerCommand(
		id: string,
		options: PluginCommandOptions,
		handler: (ctx: PluginCommandContext) => void | Promise<void>,
	): CommandSubscription

	/**
	 * Register a tool that the agent can call.
	 * Plugin tools appear alongside built-in tools and can be invoked by the LLM during conversations.
	 *
	 * @param definition - The tool definition including name, description, schema, and execute handler.
	 *
	 * @example
	 * ```ts
	 * amp.registerTool({
	 *   name: 'hello',
	 *   description: 'Greet someone by name',
	 *   inputSchema: {
	 *     type: 'object',
	 *     properties: { name: { type: 'string', description: 'Name to greet' } },
	 *     required: ['name'],
	 *   },
	 *   async execute(input) {
	 *     return `Hello, ${input.name}!`
	 *   },
	 * })
	 * ```
	 */
	registerTool(definition: PluginToolDefinition): Subscription

	/**
	 * Register a durable generic webhook for this plugin and the owning Orb thread.
	 *
	 * The key is stable within the plugin and thread. Re-registering the same key,
	 * including after a plugin reload, returns the same capability URL. Treat the
	 * URL as a credential.
	 *
	 * Handler side effects are delivered at least once. Use `event.id` as an
	 * idempotency key because an executor can stop after the handler succeeds but
	 * before durable consumption is recorded. Handlers have 30 seconds to complete;
	 * `ctx.signal` is aborted when that deadline elapses, and the event is retried.
	 *
	 * @experimental
	 * @example
	 * ```ts
	 * const { url } = await amp.createWebhook({
	 *   key: 'deploy',
	 *   handler: async (event, ctx) => {
	 *     ctx.logger.log('Deployment received', event.id, event.payload)
	 *   },
	 * })
	 * ```
	 */
	createWebhook(options: CreateWebhookOptions): Promise<WebhookRegistration>

	/** AI helpers */
	ai: PluginAI

	/**
	 * Create a custom agent bound to this plugin runtime.
	 *
	 * Run `amp plugins show-agent-options` or `amp plugins show-agent-options --json` to
	 * discover public model IDs and built-in tool names that are suitable for plugin agents.
	 */
	createAgent(config: CreateAgentConfig): Agent

	/**
	 * Get an agent handle for one of Amp's built-in agent modes (`low`,
	 * `medium`, `high`, or `ultra`). Threads spawned from the handle run the
	 * built-in mode's prompt and tools, like a thread the user started in that
	 * mode. Deprecated modes (`smart`, `deep`, `rush`) are accepted for
	 * backward compatibility and spawn threads in their replacement mode.
	 */
	getBuiltinAgent(mode: BuiltinAgentMode): Agent

	/**
	 * Register a custom agent mode that clients may show alongside built-in modes.
	 *
	 * External plugins must include a matching `// @amp-agent-mode ...` metadata
	 * comment with the mode `key` and `label` for each registered mode. Clients
	 * use this static metadata to avoid silent drift between runtime registration
	 * and discovery, and warn when they are out of sync.
	 * Multiple mode comments in one plugin file are supported.
	 */
	registerAgentMode(definition: PluginAgentModeDefinition): Subscription

	/**
	 * Observable that emits the currently active thread (the one the user is focused on
	 * in the UI), or `null` when no thread is active.
	 *
	 * Use this to determine whether the thread that triggered an event is the one the user
	 * is currently looking at, or is running in the background. For example, in a
	 * `tool.call` handler, compare `event.thread.id` to `amp.activeThread.current` to
	 * decide whether to surface a UI prompt (active) or take a non-interactive default
	 * (background).
	 */
	activeThread: Observable<{ id: ThreadID } | null> & {
		readonly current: { id: ThreadID } | null
	}

	/** Thread lookup APIs. */
	threads: PluginThreads

	/**
	 * Experimental plugin APIs that are not stable and may change or be removed.
	 *
	 * Prefer the first-class top-level APIs when available. Migrated APIs remain
	 * here as compatibility aliases for existing plugins.
	 *
	 * Agents should only build on these APIs when the user explicitly approves the
	 * use of experimental Amp plugin APIs.
	 */
	experimental?: ExperimentalPluginAPI
}

/**
 * APIs under `PluginAPI.experimental` are not stable and may change or be removed.
 */
export interface ExperimentalPluginAPI {
	/**
	 * Create a custom agent bound to this plugin runtime.
	 *
	 * Run `amp plugins show-agent-options` or `amp plugins show-agent-options --json` to
	 * discover public model IDs and built-in tool names that are suitable for plugin agents.
	 */
	createAgent(config: CreateAgentConfig): Agent

	/**
	 * Get an agent handle for one of Amp's built-in agent modes (`low`,
	 * `medium`, `high`, or `ultra`). Threads spawned from the handle run the
	 * built-in mode's prompt and tools, like a thread the user started in that
	 * mode. Deprecated modes (`smart`, `deep`, `rush`) are accepted for
	 * backward compatibility and spawn threads in their replacement mode.
	 */
	getBuiltinAgent(mode: BuiltinAgentMode): Agent

	/**
	 * Register a custom agent mode that clients may show alongside built-in modes.
	 *
	 * External plugins must include a matching `// @amp-agent-mode ...` metadata
	 * comment with the mode `key` and `label` for each registered mode. Clients
	 * use this static metadata to avoid silent drift between runtime registration
	 * and discovery, and warn when they are out of sync.
	 * Multiple mode comments in one plugin file are supported.
	 */
	registerAgentMode(definition: PluginAgentModeDefinition): Subscription

	/**
	 * Create a status item shown near the prompt editor or status bar in the Amp client.
	 *
	 * If no initial value is provided, the item is hidden until its first update.
	 */
	createStatusItem(initial?: StatusItemValue): StatusItem

	/**
	 * Observable that emits the currently active thread (the one the user is focused on
	 * in the UI), or `null` when no thread is active.
	 *
	 * Use this to determine whether the thread that triggered an event is the one the user
	 * is currently looking at, or is running in the background. For example, in a
	 * `tool.call` handler, compare `event.thread.id` to
	 * `amp.activeThread.current` to decide whether to surface a UI prompt
	 * (active) or take a non-interactive default (background).
	 */
	activeThread: Observable<{ id: ThreadID } | null> & {
		readonly current: { id: ThreadID } | null
	}

	/** Thread lookup APIs. */
	threads: PluginThreads
}

/** Reasoning effort levels supported by plugin agents, for models that support them. */
export type AgentReasoningEffort =
	| 'none'
	| 'minimal'
	| 'low'
	| 'medium'
	| 'high'
	| 'xhigh'
	| 'max'

/**
 * Amp built-in agent modes available to plugins. The `smart`, `deep`, and
 * `rush` modes are deprecated: existing threads in those modes keep working,
 * but new threads spawned from them start in the replacement mode
 * (`rush` → `low`; `smart`/`deep` → `medium`).
 */
export type BuiltinAgentMode = 'low' | 'medium' | 'high' | 'ultra' | 'smart' | 'deep' | 'rush'

export type AgentToolSelection =
	| readonly string[]
	| 'all'
	| {
			/** Tool names to include. Defaults to all tools. */
			include?: readonly string[] | 'all'
			/** Tool names to exclude after applying include. */
			exclude?: readonly string[]
	  }

export interface CreateAgentConfig {
	/** Optional stable identifier for logs, UI, and persisted run metadata. */
	name?: string
	/**
	 * Model identifier in `provider/model` format, such as `anthropic/claude-sonnet-4-6`.
	 *
	 * Run `amp plugins show-agent-options --json` for the public model IDs intended for
	 * plugin agents.
	 */
	model: PluginAIModel
	/** Instructions appended to Amp's base agent prompt. */
	instructions: string
	/**
	 * Tools available to this agent. Use 'all' for all tools available in its runtime.
	 *
	 * Run `amp plugins show-agent-options --json` for built-in tool names intended for
	 * plugin agents.
	 */
	tools?: AgentToolSelection
	/** Optional reasoning effort override for models that support it. */
	reasoningEffort?: AgentReasoningEffort
	/**
	 * Display shown for threads running this agent. Travels with the agent
	 * definition, so threads created from it — including by other plugins via
	 * a `thread.agent()` handle — carry the label.
	 */
	display?: AgentDisplay
}

/** Display metadata for a plugin agent. */
export interface AgentDisplay {
	/** Label shown in mode pickers and thread headers, 24 characters or less. */
	label: string
	/** Optional label color as a hex RGB string, for example "#d97706". */
	color?: string
}

export interface CustomAgentDefinition extends CreateAgentConfig {
	readonly kind: 'agent-definition'
}

/** Reference to one of Amp's built-in agent modes. */
export interface BuiltinAgentDefinition {
	readonly kind: 'builtin-agent'
	mode: BuiltinAgentMode
}

export type AgentDefinition = CustomAgentDefinition | BuiltinAgentDefinition

export type AgentThreadExecutor =
	| 'local'
	| 'orb'
	| {
			type: 'runner'
			/** Stable ID of a live Amp runner process. */
			id: string
	  }

export interface RunAgentOptions {
	/** Maximum time to wait for the agent run to finish, in milliseconds (default 10 minutes). */
	timeoutMs?: number
	/**
	 * Parent thread for this run when the agent is being used as a subagent/tool.
	 * When omitted, the thread is created without a parent.
	 */
	parentThreadID?: ThreadID
	/**
	 * Where the thread should execute. Defaults to `local`, which uses the current
	 * client as the executor. Use `orb` for Amp's cloud sandbox or a runner target
	 * for a live Amp runner process such as `amp --no-tui`.
	 */
	executor?: AgentThreadExecutor
}

export interface CreateAgentThreadOptions {
	/**
	 * Parent thread for the new thread when the agent is being used as a
	 * subagent/tool. When omitted, the thread is created without a parent.
	 */
	parentThreadID?: ThreadID
	/** Show the created thread and make it active in the client when supported. */
	show?: boolean
	/**
	 * Where the thread should execute. Defaults to `local`, which uses the current
	 * client as the executor. Use `orb` for Amp's cloud sandbox or a runner target
	 * for a live Amp runner process such as `amp --no-tui`.
	 */
	executor?: AgentThreadExecutor
}

/** Thread handle returned by {@link Agent.createThread}. */
export type AgentThread = PluginThread

export interface AgentRunResult {
	/** Thread created for this run. */
	threadID: `T-${string}`
	/** Final text response from the agent. */
	text: string
}

/**
 * A handle to a custom or built-in agent, returned by
 * {@link PluginAPI.createAgent} and {@link PluginAPI.getBuiltinAgent}.
 */
export interface Agent {
	readonly definition: AgentDefinition

	/**
	 * Create a background thread running this agent and return a handle for
	 * interacting with it: append messages, await replies with
	 * {@link PluginThread.waitForResponse}, observe state, or cancel.
	 *
	 * The thread keeps running independently of the caller; there is no
	 * lifecycle to manage.
	 */
	createThread(options?: CreateAgentThreadOptions): Promise<AgentThread>

	/**
	 * One-shot run: create a thread, send the message, and resolve with the
	 * assistant's reply once the turn finishes.
	 *
	 * When called from inside an executing plugin tool, aborting that tool
	 * cancels the agent's turn.
	 */
	run(message: string, options?: RunAgentOptions): Promise<AgentRunResult>
}

/** A plugin-defined agent mode shown by supported Amp clients. */
export interface PluginAgentModeDefinition {
	/** Stable identifier within the plugin. */
	key: string
	/**
	 * Label shown in compact mode pickers, for example "review" or "architect".
	 * Defaults to the agent definition's `display.label`; required when the
	 * agent has no display.
	 */
	label?: string
	/** Optional longer description shown in command palettes and pickers. */
	description?: string
	/**
	 * Optional label color as a hex RGB string, for example "#d97706".
	 * Defaults to the agent definition's `display.color`.
	 */
	color?: string
	/** Agent definition used when creating a thread with this mode selected. */
	agent: AgentDefinition
}

export interface PluginAgentMode extends Omit<PluginAgentModeDefinition, 'agent' | 'label'> {
	/** Resolved mode label (from the definition or the agent's display). */
	label: string
	pluginName: string
	agent: AgentDefinition
}

/**
 * A plugin status item shown in supported Amp clients.
 */
export interface StatusItem extends Subscription {
	/** Update the status item content. */
	update(value: StatusItemValue): void
}

export interface StatusItemValue {
	/** Text to show. */
	text: string

	/**
	 * URL to open when clicked, if any.
	 *
	 * Use a `command:` URI to execute a command registered by a plugin or the
	 * command palette. For example, `command:foo` runs the command with ID `foo`.
	 */
	url?: string
}

/**
 * Result from an AI ask operation.
 */
export interface PluginAIAskResult {
	/** The classification result: 'yes', 'no', or 'uncertain' */
	result: 'yes' | 'no' | 'uncertain'

	/** Probability (0-1) that the answer is yes */
	probability: number

	/** Explanation of why the AI gave this answer */
	reason: string
}

export type PluginAIModelProvider =
	| 'amp'
	| 'anthropic'
	| 'baseten'
	| 'fireworks'
	| 'openai'
	| 'vertexai'
	| 'xai'

/** Model identifier in `provider/model` format, such as `openai/gpt-5.6-sol`. */
export type PluginAIModel = `${PluginAIModelProvider}/${string}`



/**
 * Options for an AI operation.
 */
export interface PluginAIOptions {
	/** Thread to bill and route the AI request through. Required outside a thread-bound handler. */
	threadID?: ThreadID
	/** Model identifier in `provider/model` format. Defaults to Amp's fast classifier model. */
	model?: PluginAIModel
	/** Optional reasoning effort override for models that support it. Defaults to no reasoning. */
	reasoningEffort?: AgentReasoningEffort
	/** Optional system instructions. */
	system?: string
	/** Maximum output tokens. */
	maxTokens?: number
}

/**
 * Options for an AI ask operation.
 */
export interface PluginAIAskOptions extends PluginAIOptions {}


export interface PluginAIGenerateTextRequest extends PluginAIOptions {
	/** The prompt to send to the model. */
	prompt: string
	/** Omit schema to receive a text response. */
	schema?: never
}

export interface PluginAIGenerateObjectRequest extends PluginAIOptions {
	/** The prompt to send to the model. */
	prompt: string
	/** Schema for structured output. Supplying a schema returns the validated object. */
	schema: PluginAIObjectSchema
}

/**
 * AI capabilities provided to plugins.
 */
export interface PluginAI {
	/**
	 * Ask an AI model for a text response.
	 *
	 * Calls are routed through the current thread. Pass `threadID` when calling outside a
	 * thread-bound handler.
	 */
	generate(request: PluginAIGenerateTextRequest): Promise<string>

	/**
	 * Ask an AI model for a structured JSON object matching the provided schema.
	 *
	 * Calls are routed through the current thread. Pass `threadID` when calling outside a
	 * thread-bound handler.
	 */
	generate<T extends Record<string, unknown> = Record<string, unknown>>(
		request: PluginAIGenerateObjectRequest,
	): Promise<T>

	/**
	 * Ask an AI model a yes/no question and get a confidence-based response with reasoning.
	 * This is a convenience wrapper around {@link PluginAI.generate}.
	 *
	 * @param question - The yes/no question to ask
	 * @param options - Thread to bill and route the AI request through. Omit inside a thread-bound handler.
	 * @returns Object with result, probability, and reason
	 */
	ask(question: string, options?: PluginAIAskOptions): Promise<PluginAIAskResult>
}

/**
 * Observer interface for subscribing to configuration changes.
 */
export interface PluginConfigurationObserver<T> {
	next?(value: T): void

	error?(error: unknown): void

	complete?(): void
}

/**
 * Subscription that can be unsubscribed to release resources.
 */
export interface Subscription {
	unsubscribe(): void
}

/**
 * Target for configuration updates.
 */
export type PluginConfigurationTarget = 'workspace' | 'global'

/**
 * Minimal Observable interface used by plugin APIs that stream values over time.
 *
 * Subscribers receive subsequent values until they unsubscribe.
 */
export interface Observable<T> {
	/**
	 * Subscribe to values emitted by this observable.
	 */
	subscribe(observer: PluginConfigurationObserver<T>): Subscription
	subscribe(onNext: (value: T) => void): Subscription

	/**
	 * Pipe operators for transforming this observable.
	 */
	pipe<Out>(op: (input: Observable<T>) => Out): Out

	/**
	 * Return this observable for interop with observable libraries.
	 */
	[Symbol.observable](): Observable<T>
}

/**
 * Observable-like interface for Amp configuration.
 * Provides a limited subset of Observable functionality for plugins.
 */
export interface PluginConfiguration<T> extends Observable<T> {
	/**
	 * Get the current configuration.
	 */
	get(): Promise<T>

	/**
	 * Update configuration with partial values.
	 * @param partial - The partial configuration to merge
	 * @param target - Where to store the setting: 'global' (user settings) or 'workspace' (default)
	 */
	update(partial: Partial<T>, target?: PluginConfigurationTarget): Promise<void>

	/**
	 * Delete a configuration key.
	 * @param key - The key to delete
	 * @param target - Where to delete from: 'global' (user settings) or 'workspace' (default)
	 */
	delete(key: keyof T, target?: PluginConfigurationTarget): Promise<void>
}

/**
 * Logger provided to plugins for scoped logging.
 */
export interface PluginLogger {
	log: (...args: unknown[]) => void
}

/**
 * Bun shell function type (simplified version of Bun.$)
 */
export type ShellFunction = (
	strings: TemplateStringsArray,
	...values: unknown[]
) => Promise<ShellResult>

/**
 * Result from a shell command execution.
 */
export interface ShellResult {
	exitCode: number
	stdout: string
	stderr: string
}

/**
 * Where plugin code is running relative to the interactive UI.
 */
export type PluginExecutorKind = 'local' | 'remote' | 'unknown'

/**
 * Information about the executor running plugin code.
 */
export interface PluginExecutor {
	readonly kind: PluginExecutorKind

	/**
	 * Keep the current orb awake until the returned subscription is unsubscribed or the plugin
	 * process exits. The lease renews automatically and consumes orb runtime credits.
	 *
	 * Rejects when the plugin is not running inside an Amp-managed orb. This is a best-effort
	 * lease: manual pauses, insufficient credits, and provider runtime limits may still pause the
	 * orb.
	 */
	keepAlive(): Promise<Subscription>
}

/**
 * Identity of the authenticated Amp user exposed to plugins.
 */
export interface User {
	/**
	 * Opaque string that identifies the user.
	 */
	readonly id: string

	/** User's email address. */
	readonly email: string

	/** User's first name, when set. */
	readonly firstName: string | null

	/** User's last name, when set. */
	readonly lastName: string | null

	/** User's Amp username, when set. */
	readonly username: string | null

	/** Workspace the user belongs to, or null when the user is not in a workspace. */
	readonly workspace: Workspace | null
}

/**
 * Workspace identity for the authenticated user.
 */
export interface Workspace {
	/** Opaque string that identifies the workspace. */
	readonly id: string

	/** Workspace slug/name. */
	readonly name: string

	/** Human-friendly workspace display name, when set. */
	readonly displayName: string | null
}

/**
 * System capabilities and information provided to plugins.
 */
export interface PluginSystem {
	/**
	 * Open a URL using the system's default protocol handler.
	 * On the CLI, it also shows a dialog with the URL text (for SSH users who can't open URLs remotely).
	 */
	open(url: string | URL): Promise<void>

	/**
	 * Root of the workspace or repository the user has open, or null when Amp is
	 * running without a workspace. This is stable for the plugin process lifetime;
	 * plugins are reloaded when the workspace changes.
	 *
	 * Use {@link PluginAPI.helpers.filePathFromURI} to convert this file URI to a
	 * local filesystem path before running workspace-relative shell commands.
	 */
	readonly workspaceRoot: URI | null

	/**
	 * Get the effective Amp base URL currently used by this Amp client.
	 * This reflects the active runtime configuration (for example, custom domains via `AMP_URL`).
	 */
	readonly ampURL: URL

	/**
	 * Identity of the authenticated Amp user, or null when Amp is not authenticated.
	 */
	readonly user: User | null

	/**
	 * Information about the executor that is running this plugin.
	 */
	readonly executor: PluginExecutor
}

/** @internal */
export type SpanID = string & { readonly __brand: 'SpanID' }

export type ThreadID = `T-${string}`

/**
 * Message IDs are numeric in legacy TUI threads and stable string IDs in Neo
 * thread-actor threads.
 */
export type ThreadMessageID = number | string

/**
 * A text content block in a message.
 */
export interface ThreadTextBlock {
	type: 'text'
	text: string
}

/**
 * A thinking content block in a message.
 */
export interface ThreadThinkingBlock {
	type: 'thinking'
	thinking: string
}

/**
 * A tool use content block in a message.
 */
export interface ThreadToolUseBlock {
	type: 'tool_use'
	id: string
	name: string
	input: Record<string, unknown>
}

/**
 * A tool result content block in a message.
 */
export interface ThreadToolResultBlock {
	type: 'tool_result'
	toolUseID: string
	output?: PluginToolResult
	status: 'done' | 'error' | 'cancelled' | 'running' | 'pending'
}

/**
 * A user message in the thread.
 */
export interface ThreadUserMessage {
	role: 'user'

	/** The message ID, which is unique in the thread. */
	id: ThreadMessageID

	content: (ThreadTextBlock | ThreadToolResultBlock)[]
}

/**
 * An assistant message in the thread.
 */
export interface ThreadAssistantMessage {
	role: 'assistant'

	/** The message ID, which is unique in the thread. */
	id: ThreadMessageID

	content: (ThreadTextBlock | ThreadThinkingBlock | ThreadToolUseBlock)[]
}

/**
 * An info message in the thread.
 */
export interface ThreadInfoMessage {
	role: 'info'

	/** The message ID, which is unique in the thread. */
	id: ThreadMessageID

	content: ThreadTextBlock[]
}

/**
 * A message in the thread (simplified view for plugins).
 */
export type ThreadMessage = ThreadUserMessage | ThreadAssistantMessage | ThreadInfoMessage

/**
 * Options for reading messages from a thread.
 */
export interface ThreadMessagesOptions {
	/**
	 * When true, read the full transcript, including messages that have been
	 * compacted away.
	 *
	 * By default, messages are read as a new inference turn would see them:
	 * when the thread has been compacted, the latest compaction summary
	 * (as a user message) followed by the messages after the compaction cut
	 * point; otherwise the full transcript.
	 */
	full?: boolean

	/**
	 * Where to read from. Defaults to `end` so callers read recent messages by
	 * default instead of accidentally loading the start of a large thread.
	 */
	from?: 'start' | 'end'

	/**
	 * What the offset is in relation to `from`.  Defaults to 0.
	 */
	offset?: number

	/**
	 * Maximum number of messages to return. Clamped to 20.
	 */
	limit?: number

	/**
	 * Optional role filter.
	 */
	roles?: Array<'user' | 'assistant'>
}

/**
 * Agent activity state of a thread.
 *
 * - `idle`: the agent is not working; the last turn (if any) has finished.
 * - `running`: the agent is working (inference or tool execution in progress).
 * - `awaiting-approval`: the agent is blocked waiting for a tool approval.
 * - `error`: the thread has an active error.
 */
export type ThreadState = 'idle' | 'running' | 'awaiting-approval' | 'error'

/**
 * Thread API for reading and manipulating the current thread.
 */
export interface PluginThread {
	/** Active thread ID for the current invocation context */
	id: ThreadID

	/** Agent currently used by this thread, suitable for creating related threads. */
	agent(): Promise<Agent>

	/** Current thread title stream, or `null` when no title has been set yet. */
	readonly title: Observable<string | null> & { get(): Promise<string | null> }

	/** Agent activity state stream for this thread. */
	readonly state: Observable<ThreadState> & { get(): Promise<ThreadState> }

	/**
	 * Wait for the current or next agent turn to finish and resolve with the
	 * assistant's reply.
	 *
	 * Waits until the thread has been `running` (or `awaiting-approval`) and
	 * returns to `idle`, then resolves with the last assistant message.
	 * Rejects if the thread enters the `error` state or the timeout elapses
	 * (default 10 minutes).
	 */
	waitForResponse(options?: { timeoutMs?: number }): Promise<ThreadAssistantMessage>

	/** Stop the agent's current turn in this thread, if one is running. */
	cancel(): Promise<void>

	/**
	 * Read messages from the thread in a stable plugin-facing schema.
	 *
	 * By default this reads the messages a new inference turn would see:
	 * after a compaction, the latest compaction summary and the messages
	 * from the compaction cut point onward. Pass `full: true` to read the
	 * entire transcript, including compacted-away messages.
	 *
	 * Defaults to `{ from: 'end', limit: 10 }`. The maximum `limit`
	 * is 20.  Combine with `offset` to fetch more messages.
	 */
	messages(options?: ThreadMessagesOptions): Promise<ThreadMessage[]>

	/**
	 * Append a user message to the thread.
	 */
	append(messages: UserMessage[]): Promise<void>

	/**
	 * Append a single user message to the thread.
	 *
	 * When `steer` is true and the thread is busy, the message is queued as a
	 * steering message and is preferred when the thread next dequeues work.
	 */
	appendUserMessage(message: UserMessage, options?: AppendUserMessageOptions): Promise<void>
}

/** APIs for accessing threads by ID. */
export interface PluginThreads {
	/** Get a thread handle for the given thread ID. */
	get(threadID: ThreadID): PluginThread
}

/**
 * A user message that can be appended to the thread.
 */
export interface UserMessage {
	type: 'user-message'

	content: string
}

/** Options for appending a single user message to a thread. */
export interface AppendUserMessageOptions {
	/**
	 * Prefer this message when it is queued behind in-progress work.
	 */
	steer?: boolean
}

/**
 * Options for the input dialog.
 */
export interface PluginInputOptions {
	/** Dialog title */
	title?: string

	/** Help text/description shown below the title */
	helpText?: string

	/** Initial text value in the input field */
	initialValue?: string

	/** Text for the submit button (default: "Submit") */
	submitButtonText?: string
}

/**
 * Options for the confirm dialog.
 */
export interface PluginConfirmOptions {
	/** Dialog title */
	title: string

	/** Message body shown below the title */
	message?: string

	/** Text for the confirm button (default: "Yes") */
	confirmButtonText?: string
}

/**
 * Options for the select dialog.
 */
export interface PluginSelectOptions {
	/** Dialog title */
	title: string

	/** Message body shown below the title */
	message?: string

	/** Initially selected option value */
	initialValue?: string

	/** Entries to display as choices */
	options: string[]
}

/**
 * UI capabilities provided to plugins.
 */
export interface PluginUI {
	notify(message: string): Promise<void>

	/**
	 * Show an input dialog prompting the user for text input.
	 * @returns The entered text, or undefined if the user cancelled.
	 */
	input(options: PluginInputOptions): Promise<string | undefined>

	/**
	 * Show a confirmation dialog with Yes/No options.
	 * @returns true if the user confirmed, false if they cancelled.
	 */
	confirm(options: PluginConfirmOptions): Promise<boolean>

	/**
	 * Show a select dialog with user provided options.
	 * @returns the selected value, undefined if they cancelled
	 */
	select(options: PluginSelectOptions): Promise<string | undefined>
}

/**
 * URI value returned by helper APIs.
 *
 * This stays intentionally minimal so external plugin authors don't need
 * Amp's internal URI package in their dependency graph.
 */
export interface URI {
	toString(): string
}

/**
 * Event payload for session.start event.
 * Fired when Amp starts a thread session, such as when the user sends the first
 * message in a new thread or opens/switches to an existing thread.
 */
export interface SessionStartEvent {
	/** The thread that started running */
	thread: { id: ThreadID }
}

/**
 * A tool call.
 */
export interface ToolCall {
	/** Unique identifier for this tool use (e.g., "toolu_xxx") */
	toolUseID: string

	/** Name of the tool that will be executed */
	tool: string

	/** Input arguments that will be passed to the tool */
	input: Record<string, unknown>
}

/**
 * Event payload for tool.call event.
 * This is a request that expects a response from the handler.
 */
export interface ToolCallEvent extends ToolCall {
	/** The active thread reference for this tool invocation. */
	thread: { id: ThreadID }
}

/**
 * Result returned from a tool.call handler.
 * Determines how the tool execution should proceed.
 */
export type ToolCallResult =
	/** Allow the tool to execute with its original input */
	| { action: 'allow' }

	/** Reject the tool call but allow the agent to continue with other tools */
	| { action: 'reject-and-continue'; message: string }

	/** Modify the tool's input arguments before execution */
	| { action: 'modify'; input: Record<string, unknown> }

	/** Provide a synthesized result without actually running the tool */
	| { action: 'synthesize'; result: { output: string; exitCode?: number } }

	/** Error occurred in the plugin - stops the thread worker and shows an ephemeral error */
	| { action: 'error'; message: string }

/**
 * A terminal tool result.
 */
export interface ToolResult {
	/** Unique identifier for this tool use (e.g., "toolu_xxx") */
	toolUseID: string

	/** Name of the tool that was executed */
	tool: string

	/** Input arguments passed to the tool */
	input: Record<string, unknown>

	/** Result status of the tool execution */
	status: 'done' | 'error' | 'cancelled'

	/** Error message if status is 'error' */
	error?: string

	/** Tool output/result if available */
	output?: unknown
}

/**
 * A structured content block returned from a plugin tool.
 */
export type PluginToolResultContentBlock =
	| { type: 'text'; text: string }
	| {
			type: 'image'

			/** MIME type, e.g. 'image/png', 'image/jpeg', or 'image/webp'. */
			mimeType: string

			/** Base64-encoded payload with no data: prefix. */
			data: string
	  }

/**
 * Result returned from a plugin tool.
 *
 * Returning a bare string keeps the existing text-only behavior. Returning an array
 * of content blocks lets a tool mix text and inline base64 image blocks.
 */
export type PluginToolResult = string | PluginToolResultContentBlock[]

/**
 * Event payload for tool.result event.
 */
export interface ToolResultEvent extends ToolResult {
	/** The active thread for this tool result */
	thread: { id: ThreadID }
}

/**
 * Result returned from a tool.result handler.
 * Allows modifying the tool result before it is sent back to the model.
 */
export type ToolResultResult =
	| {
			status: 'done'
			output?: unknown
	  }
	| {
			status: 'error'
			error?: string
			output?: unknown
	  }
	| {
			status: 'cancelled'
			error?: string
			output?: unknown
	  }
	| undefined
	| void

/**
 * Event payload for agent.start event.
 * Fired when a user submits a prompt (initial or reply).
 */
export interface AgentStartEvent {
	/** The active thread for this agent turn */
	thread: { id: ThreadID }

	/** The user's prompt message */
	message: string

	/** The message ID */
	id: ThreadMessageID
}

/**
 * Result returned from an agent.start handler.
 * Allows adding context messages or modifying the system prompt.
 */
export interface AgentStartResult {
	/**
	 * A message to append after the user's content in the user message.
	 * If display is true, the message is shown in the UI. Defaults to false.
	 */
	message?: { content: string; display?: boolean }
}

/**
 * Event payload for agent.end event.
 * Fired when the agent finishes handling a user prompt.
 */
export interface AgentEndEvent {
	/** The active thread for this agent turn */
	thread: { id: ThreadID }

	/** The user's prompt message that started this turn */
	message: string

	/** The message ID that started this turn */
	id: ThreadMessageID

	/** The outcome of the agent's turn */
	status: 'done' | 'error' | 'cancelled'

	/** All messages since the agent.start event (including the user message that started this turn) */
	messages: ThreadMessage[]
}

/**
 * Result returned from an agent.end handler.
 * Allows starting a new agent turn by returning a user message.
 */
export type AgentEndResult =
	/** Automatically send a follow-up user message to start a new agent turn */
	{ action: 'continue'; userMessage: string } | void

/**
 * Map of event names to their payload types.
 */
export interface PluginEventMap {
	'session.start': SessionStartEvent
	'tool.call': ToolCallEvent
	'tool.result': ToolResultEvent
	'agent.start': AgentStartEvent
	'agent.end': AgentEndEvent
}

/**
 * Map of request event names to their result types.
 * These events expect a response from the handler.
 */
export interface PluginRequestResultMap {
	'tool.call': ToolCallResult
	'tool.result': ToolResultResult
	'agent.start': AgentStartResult
	'agent.end': AgentEndResult
}

/**
 * Context shared by all plugin event handlers.
 */
export interface PluginEventContextBase {
	/** Scoped logger for plugin output. Log messages are appended to the handler's trace span events. */
	logger: PluginLogger

	/** Bun's shell API for executing commands */
	$: ShellFunction

	/** Platform UI capabilities */
	ui: PluginUI

	/** AI capabilities */
	ai: PluginAI

	/** System capabilities and information */
	system: PluginSystem

	/** The trace span ID for this handler invocation, if tracing is enabled */
	span?: SpanID
}

/**
 * Context passed as the second argument to event handlers.
 * All plugin events are thread-scoped.
 */
export type PluginEventContext<E extends keyof PluginEventMap> = PluginEventContextBase & {
	thread: PluginThread
}

/**
 * Handler return type based on whether the event expects a response.
 * Request events (in PluginRequestResultMap) must return a result.
 * Fire-and-forget events return void.
 */
export type PluginHandlerResult<E extends keyof PluginEventMap> =
	E extends keyof PluginRequestResultMap
		? PluginRequestResultMap[E] | Promise<PluginRequestResultMap[E]>
		: void | Promise<void>

/**
 * Standardized shell command representation.
 */
export interface ShellCommand {
	command: string
	dir?: string
}

/**
 * A tool call and its corresponding terminal tool result extracted from thread messages.
 */
export interface ToolCallWithResult {
	call: ToolCall
	result: ToolResult
}

/**
 * Extracts the shell command from a Bash or shell_command tool call.
 * Returns null if the event is not a shell command tool call.
 */
export type ShellCommandFromToolCall = (event: ToolCall) => ShellCommand | null

/**
 * Extracts paired tool calls and terminal tool results from a list of thread messages.
 */
export type ToolCallsInMessages = (messages: ThreadMessage[]) => ToolCallWithResult[]

/**
 * Returns an array of file URIs modified by a tool call, or null if the tool doesn't modify files.
 * Supports edit/create/apply_patch tools and sed in-place shell commands.
 */
export type FilesModifiedByToolCall = (event: ToolCall | ToolResult) => URI[] | null

/**
 * Converts a file URI returned by helper APIs to a local filesystem path.
 */
export type FilePathFromURI = (uri: URI) => string

/**
 * Determines whether an instance of Error indicates that no Plugin UI is available.
 */
export type IsPluginUINotAvailableError = (error: Error) => boolean

/**
 * Whether a registered command is selectable in the command palette.
 *
 * - `enabled`: shown and selectable.
 * - `disabled`: shown but not selectable; `reason` is displayed alongside the command.
 * - `hidden`: not shown in the palette at all.
 */
export type CommandAvailability =
	| { type: 'enabled' }
	| { type: 'disabled'; reason: string }
	| { type: 'hidden' }

/**
 * Options for registering a command.
 */
export interface PluginCommandOptions {
	/** The title shown after the colon in the command palette (e.g., "Greet" in "Hello: Greet") */
	title: string

	/** The category shown before the colon (e.g., "Hello" in "Hello: Greet"). Defaults to the plugin name. */
	category?: string

	/** Human-readable description of what this command does */
	description?: string

	/**
	 * Initial availability of the command in the command palette.
	 * Defaults to `{ type: 'enabled' }`.
	 *
	 * Use the {@link CommandSubscription.setAvailability} method on the
	 * subscription returned by {@link PluginAPI.registerCommand} to update
	 * availability dynamically.
	 */
	availability?: CommandAvailability
}

/**
 * Subscription returned by {@link PluginAPI.registerCommand}.
 *
 * Allows updating the command's availability in the palette in addition to
 * unregistering it.
 */
export interface CommandSubscription extends Subscription {
	/**
	 * Update whether this command is selectable in the command palette.
	 * Triggers a refresh in the host so the palette reflects the new state
	 * on its next read.
	 */
	setAvailability(status: CommandAvailability): void
}

/**
 * Context passed to command handlers.
 * Provides access to UI capabilities for executing command actions.
 */
export interface PluginCommandContext {
	/** Platform UI capabilities */
	ui: PluginUI

	/** AI capabilities */
	ai: PluginAI

	/** System capabilities and information */
	system: PluginSystem

	/** Bun's shell API for executing commands */
	$: ShellFunction

	/**
	 * Current thread context if a thread is active, or `undefined` when the
	 * user has not started one yet. To create a thread from a command, use
	 * `amp.getBuiltinAgent(...)` or `amp.createAgent(...)` and call
	 * `agent.createThread({ show: true })`, then append to the returned thread.
	 */
	thread?: PluginThread
}

/**
 * Context passed to tool execute handlers.
 */
export interface PluginToolContext {
	/** UI capabilities provided to plugins */
	ui: PluginUI

	/** Scoped logger for plugin output */
	logger: PluginLogger

	/** Current thread context for this tool invocation */
	thread: PluginThread
}

/** A provider-neutral webhook event delivered to a plugin handler. */
export interface WebhookEvent {
	/** Stable server-owned event ID. Use this to make handler effects idempotent. */
	id: string

	/** JSON-compatible request payload accepted by the generic webhook endpoint. */
	payload: unknown

	/** Bounded string metadata recorded by the webhook ingress. */
	metadata: Readonly<Record<string, string>>

	/** ISO 8601 timestamp recorded when the server accepted the webhook. */
	receivedAt: string
}

/** Context passed to webhook handlers for the owning thread. */
export interface WebhookHandlerContext extends PluginEventContextBase {
	/** Thread that owns this webhook registration. */
	thread: PluginThread

	/** Aborted when the handler's execution deadline elapses. */
	signal: AbortSignal
}

/** Options for registering a generic webhook handler. */
export interface CreateWebhookOptions {
	/** Stable, non-whitespace-padded key within this plugin and thread (1-128 characters). */
	key: string

	/** At-least-once handler for matching durable webhook events. */
	handler: (event: WebhookEvent, ctx: WebhookHandlerContext) => void | Promise<void>
}

/** Safe registration information returned to the plugin. */
export interface WebhookRegistration {
	/** Capability URL for webhook POST requests. Treat this URL as a credential. */
	url: string
}

/**
 * Options for registering a tool that the agent can call.
 */
export interface PluginToolDefinition {
	/** Tool name (must match ^[a-zA-Z0-9_-]+$) */
	name: string

	/** Description shown to the LLM explaining what the tool does */
	description: string

	/** JSON Schema for the tool's input parameters */
	inputSchema: {
		type: 'object'
		properties?: Record<string, object>
		required?: string[]
		[key: string]: unknown
	}

	/** Execute the tool with the given input and return a result */
	execute: (
		input: Record<string, unknown>,
		ctx: PluginToolContext,
	) => Promise<PluginToolResult | void>
}