· 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) {
				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}`,
					`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) => {
			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,它会自动启动一个后续回合。

@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 environment 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

	/** AI helpers */
	ai: PluginAI

	/**
	 * Experimental plugin APIs that are not stable and may change or be removed.
	 *
	 * 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 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.experimental.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
	}
}

/**
 * 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
}

/**
 * AI capabilities provided to plugins.
 */
export interface PluginAI {
	/**
	 * Ask an AI model a yes/no question and get a confidence-based response with reasoning.
	 * @param question - The yes/no question to ask
	 * @returns Object with result, probability, and reason
	 */
	ask(question: string): 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 value.
	 */
	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
}

/**
 * System capabilities 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>

	/**
	 * 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

	/**
	 * 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

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

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

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

	content: string
}

/**
 * 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 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.
	 */
	message?: { content: string; display: true }
}

/**
 * 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 */
	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 */
	system: PluginSystem

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

	/** Current thread context if a thread is active, undefined otherwise */
	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
}

/**
 * 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>
}