请参阅 插件指南 ,了解设置、能力和完整示例。本页包含一个端到端插件以及生成的 @ampcode/plugin 类型参考。
这个单一插件演示了所有插件能力:事件、命令、工具、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}`
},
})
}
可以用以下提示词和命令试用:
kitchen-sink: show kitchen sink notification。kitchen-sink: run kitchen sink UI 来测试通知、输入框、选择框、确认框和追加对话。kitchen-sink: open kitchen sink docs 来测试 ctx.system.open(...)。kitchen-sink: show kitchen sink runtime 来测试配置、shell 执行和系统元数据。kitchen-sink: append kitchen sink message。Use the kitchen_sink_tool with message hello.kitchen sink this turn.kitchen sink continue.导出函数体演示了插件加载时的初始化。UI 命令演示了 ctx.ui.notify、ctx.ui.input、ctx.ui.select、ctx.ui.confirm 和 ctx.thread?.append(...)。运行时命令演示了 amp.configuration.get()、amp.$ 和 amp.system。工具提示词演示了已注册工具以及它周围的 tool.call 和 tool.result 钩子。kitchen sink this turn 提示词演示了 agent.start 和 amp.ai.ask。最后一个提示词演示了 agent.end,它会自动启动一个后续回合。
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 + '.'
},
})
}
当你希望主 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>
}