mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
* feat(server): add background agents An agent can be created background in any workspace. It stays a durable, addressable agent, but default discovery, workspace status and attention leave it out. Effective background is agent.background || workspace.background. - Protocol: `create_agent_request.background` now marks the agent itself and stays the workspace intent for a workspace the request creates. Snapshots carry `background`. The daemon advertises `features.backgroundAgents`. Clients advertise the `background_agents` capability. - Daemon: the flag persists in the agent record, so restart and resume keep it. AgentDirectory owns visibility. Inclusive reads and subscriptions from sources without `background_agents` see only agents in background workspaces, because older apps would otherwise open a tab for each one. - Inheritance: omitted on agent-scoped creation, children and new workspaces take the caller agent's background. - MCP: create_agent accepts `background`. - SDK: placing a background agent in an existing workspace, or under a caller, fails fast with "Update the host to use background agents." on older hosts. - CLI: `ls -a` and `ls --background` add a BACKGROUND column. * feat(app): hide background agents from tabs, track and status * docs(plugins): point hidden helpers at background agents * fix: keep background agents hidden across cache reload and connected hosts * fix: keep remote creation working on older hosts and inherit foreign parents' background
642 lines
35 KiB
Markdown
642 lines
35 KiB
Markdown
---
|
||
title: 插件钩子
|
||
description: 为 Agent 和工作区操作注册钩子,修改请求或接收通知。
|
||
nav: 钩子
|
||
---
|
||
|
||
# 插件钩子 {#plugin-hooks}
|
||
|
||
在 `index.server.ts` 中使用 `server.addHook(name, handler)`。钩子在插件启用时运行,即使没有连接的应用。`addHook` 从 Paseo 0.14 起可用;将 `requirements.paseo` 设置为你开发插件时使用的版本。
|
||
|
||
```ts
|
||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||
|
||
export default function contribute(server: PluginServerContext) {
|
||
const remove = server.addHook("agent.open.before", async ({ request }) => ({
|
||
...request,
|
||
env: { ...request.env, REGION: "eu" },
|
||
}));
|
||
return remove;
|
||
}
|
||
```
|
||
|
||
## 命名与等待 {#naming-and-timing}
|
||
|
||
名称遵循 `subject.operation.position`:主体、操作、`before` 或 `after`。名称标识生命周期中的位置;Paseo 是否等待由每个钩子决定,见下表。`workspace` 包括所有工作区,`worktree` 仅包括 git worktree。
|
||
|
||
- 等待型钩子结束后 Paseo 才继续。抛出错误、返回无效数据或超过 30 秒会让操作失败,错误包含 `Plugin <id> <hook> failed: …`。修改请求的钩子收到 `{ request }`,可以返回完整的替换请求或 `undefined`。
|
||
- 通知收到事件对象,返回 `Promise<void>`。Paseo 不等待;错误会记录在插件名下,原操作继续。`agent.turn.before` 和 `agent.permission.before` 分别在轮次开始或权限进入待处理状态时发出通知。
|
||
|
||
所有处理函数都必须为异步函数。轮次完成包括成功、失败和取消,请查看 `outcome`。权限完成包括所有处理结果,请查看 `resolution`。
|
||
|
||
创建 Agent 会发生一次,`agent.create.before` 修改已保存的配置。打开 Agent 指启动 Provider 进程,包括首次运行、daemon 重启后恢复、读取历史、导入外部会话和重新加载。`agent.open.before` 为每次启动提供临时的环境变量、工具和 MCP 服务器。创建会打开 Agent;打开已有 Agent 不会创建它;关闭会结束运行中的实例。在应用中打开标签页不属于此操作。
|
||
|
||
`worktree.create.after` 在工作区注册前运行,随后是 `workspace.create.after`,两者都早于第一个 Agent 的 `agent.create.before` / `agent.open.before`。
|
||
|
||
### 从等待型钩子提前返回 {#return-early-from-a-waiting-hook}
|
||
|
||
如果操作应立即继续,可以启动独立任务并返回:
|
||
|
||
```ts
|
||
server.addHook("worktree.create.after", async ({ worktree }) => {
|
||
void warmCaches(worktree.path);
|
||
});
|
||
```
|
||
|
||
Paseo 只观察返回的 Promise,因此独立任务没有错误报告,也没有取消 `signal`。
|
||
|
||
## 钩子一览 {#hooks}
|
||
|
||
| 钩子 | 等待 | 载荷 | 可修改 |
|
||
| ------------------------- | ---- | -------------------------------------- | ----------------------------------------- |
|
||
| `workspace.create.before` | 是 | `{ request }` | 整个创建请求 |
|
||
| `workspace.create.after` | 否 | `{ workspace }` | |
|
||
| `workspace.archive.after` | 否 | `{ workspace }` | |
|
||
| `worktree.create.before` | 是 | `{ request }` | 创建选项和目标路径 |
|
||
| `worktree.create.after` | 是 | `{ worktree }` | worktree 中的文件;抛出错误中止创建 |
|
||
| `agent.create.before` | 是 | `{ request: { config, env? } }` | 除 `cwd` 外的已保存配置;`env` |
|
||
| `agent.create.after` | 否 | `{ agent }` | |
|
||
| `agent.open.before` | 是 | `{ request }` | 仅用于启动的 `env`、`tools`、`mcpServers` |
|
||
| `agent.close.after` | 否 | `{ agent }` | |
|
||
| `agent.archive.after` | 否 | `{ agent, archivedAt }` | |
|
||
| `agent.turn.before` | 否 | `{ agent, turnId }` | |
|
||
| `agent.turn.after` | 否 | `{ agent, turnId, outcome, timeline }` | |
|
||
| `agent.permission.before` | 否 | `{ agent, request }` | |
|
||
| `agent.permission.after` | 否 | `{ agent, requestId, resolution }` | |
|
||
|
||
## 共享载荷 {#shared-payloads}
|
||
|
||
```ts
|
||
interface PluginHookAgent {
|
||
id: string;
|
||
workspaceId: string | null;
|
||
parentAgentId: string | null;
|
||
provider: string;
|
||
cwd: string;
|
||
title: string | null;
|
||
}
|
||
|
||
interface PluginHookWorkspace {
|
||
id: string;
|
||
projectId: string;
|
||
cwd: string;
|
||
name: string | null;
|
||
archivedAt: string | null;
|
||
}
|
||
|
||
type PluginTurnOutcome =
|
||
| { kind: "completed" }
|
||
| { kind: "failed"; error: { message: string; code?: string } }
|
||
| { kind: "canceled"; reason: string };
|
||
```
|
||
|
||
| 字段 | 结构 / 含义 |
|
||
| ------------ | ---------------------------------------------------------------------------------------- |
|
||
| `turnId` | Provider 报告的 `string` 或 `null`;会话重新打开后可能重复 |
|
||
| `timeline` | 已弃用,始终为 `[]`;通过 SDK 获取历史。 |
|
||
| `request` | SDK 的 `AgentPermissionRequest`;`kind` 为 `tool`、`plan`、`question`、`mode` 或 `other` |
|
||
| `resolution` | SDK 的 `AgentPermissionResponse` |
|
||
| `archivedAt` | 时间戳字符串 |
|
||
|
||
## workspace.create.before {#workspacecreatebefore}
|
||
|
||
收到 `{ request }`,其中包含 `source`、可选的 `title` 和 `firstAgentContext`。可替换整个显式创建请求;抛出错误、无效返回或超时会让创建失败。已有工作区和目录查找、导入不受影响。
|
||
|
||
### 选择工作区隔离方式 {#choose-workspace-isolation}
|
||
|
||
```ts
|
||
server.addHook("workspace.create.before", async ({ request }) => {
|
||
if (request.source.kind !== "directory") {
|
||
return request;
|
||
}
|
||
|
||
return {
|
||
...request,
|
||
source: {
|
||
kind: "worktree",
|
||
cwd: request.source.path,
|
||
action: "branch-off",
|
||
},
|
||
};
|
||
});
|
||
```
|
||
|
||
**结果**:显式的目录创建请求会变成 worktree 请求。已有的工作区以及目录查找/导入操作不受影响。
|
||
|
||
## workspace.create.after {#workspacecreateafter}
|
||
|
||
工作区记录创建且目录可用后触发,收到 `{ workspace }`。
|
||
|
||
```ts
|
||
server.addHook("workspace.create.after", async ({ workspace }) => {
|
||
console.log(workspace);
|
||
});
|
||
```
|
||
|
||
## workspace.archive.after {#workspacearchiveafter}
|
||
|
||
归档状态保存后触发,收到 `{ workspace }`;worktree 清理可能尚未完成。
|
||
|
||
```ts
|
||
server.addHook("workspace.archive.after", async ({ workspace }) => {
|
||
console.log(workspace);
|
||
});
|
||
```
|
||
|
||
## worktree.create.before {#worktreecreatebefore}
|
||
|
||
为新 worktree 选择路径和创建选项。通过应用、CLI、MCP 或创建 Agent 新建 worktree 时,Paseo 在 `git worktree add` 之前等待此钩子。钩子收到 `{ request }`,返回完整的替换请求,或返回 `undefined` 保持原值。抛出错误、返回无效数据或超过 30 秒会终止创建,错误包含插件和钩子名称。
|
||
|
||
```ts
|
||
import { basename, dirname, join } from "node:path";
|
||
|
||
server.addHook("worktree.create.before", async ({ request }) => ({
|
||
...request,
|
||
path: join(
|
||
dirname(request.sourceCheckout),
|
||
`${basename(request.sourceCheckout)}.${request.branch.replaceAll("/", "-")}`,
|
||
),
|
||
}));
|
||
```
|
||
|
||
源检出目录为 `/home/me/src/paseo`、分支为 `feature/x` 时,目标为 `/home/me/src/paseo.feature-x`。完整插件见 [worktree-placement 示例](https://github.com/getpaseo/paseo/tree/main/plugin-examples/worktree-placement)。
|
||
|
||
| 请求字段 | 含义 |
|
||
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||
| `kind` | `branch-off`、`checkout-branch`、`checkout-change-request` 或 `checkout-github-pr` |
|
||
| `branch` | 请求的本地分支名称 |
|
||
| `baseRef` | branch-off 的起始引用;变更请求的比较基准;`checkout-branch` 不含此字段。要从引用创建分支,返回 `kind: "branch-off"`。 |
|
||
| `slug` | 规范化的 worktree 名称 |
|
||
| `path` | 绝对目标路径,初始值为已处理现有路径后缀的默认位置 |
|
||
| `repoRoot` | 只读,主仓库根目录 |
|
||
| `sourceCheckout` | 只读,源检出根目录,也支持从另一个 worktree 发起创建 |
|
||
| `forge`、`changeRequestNumber`、`headRef` | 变更请求检出标识;旧版 GitHub 检出用 `githubPrNumber` 代替 `forge` 和 `changeRequestNumber` |
|
||
| `headRepositoryOwner`、`headRepository` | 可选的源仓库信息;`headRepository` 仅用于变更请求检出 |
|
||
| `checkoutRefs` | 可选的获取源:`{ remoteName?: string, remoteRef: string }[]` |
|
||
| `pushRemoteUrl`、`trackOriginHead` | 可选的推送目标和跟踪策略 |
|
||
|
||
返回 `path` 后,`slug` 仅用于 branch-off 分支命名;CLI 使用路径的最后一级目录名查找 worktree。
|
||
|
||
除 `repoRoot` 和 `sourceCheckout` 外,所有字段都可以修改。返回所选 `kind` 的全部必填字段。Paseo 在钩子之后解析 Git 源计划,因此钩子可以在获取引用之前修改分支和基准,而 Git 之后仍可能选择唯一的最终分支名称。
|
||
|
||
Paseo 解析父目录中的符号链接并验证返回的路径。路径必须为绝对路径且不存在,不能是文件系统根目录、用户主目录、仓库根目录、`.git` 目录内的路径或仓库的祖先目录。目标已存在时创建失败,Paseo 不会为插件选择的路径添加后缀。插件应让 Paseo 创建目录。
|
||
|
||
顺序:`worktree.create.before` → `git worktree add` → `worktree.create.after` → 注册工作区。恢复缺失的 worktree 时使用记录的原路径,不运行这两个创建钩子。
|
||
|
||
## worktree.create.after {#worktreecreateafter}
|
||
|
||
新的 git worktree 在磁盘上创建后,Paseo 会等待此钩子,然后才注册工作区、运行 setup、打开终端或启动 Agent。收到 `{ worktree: { path, branch, sourceCheckout } }`,分别是 worktree 绝对路径、分支名、来源 checkout 目录。此时还没有工作区 id。无需返回数据,直接写入 worktree 即可。抛出错误或超过 30 秒会中止创建并删除新 worktree。
|
||
|
||
此钩子覆盖新建分支、检出分支和检出 PR。恢复缺失的已归档 worktree 使用独立路径,不触发此钩子。
|
||
|
||
```ts
|
||
import { copyFile } from "node:fs/promises";
|
||
|
||
server.addHook("worktree.create.after", async ({ worktree }) => {
|
||
await copyFile(`${worktree.sourceCheckout}/.env`, `${worktree.path}/.env`);
|
||
});
|
||
```
|
||
|
||
## agent.create.before {#agentcreatebefore}
|
||
|
||
收到 `{ request: { config, env? } }`。配置修改会保存,即使插件以后被停用或移除;`env` 只用于首次启动,不会保存。不能修改 `config.cwd`。抛出错误、无效返回或超时会让创建失败。
|
||
|
||
**`agent.create.before.config`** 使用 `AgentSessionConfig`:
|
||
|
||
| 字段 | 约束 |
|
||
| --------------------------------------------- | -------------------------------------------------------- |
|
||
| `provider`, `model` | 独立的字段;修改 Provider 可能需要同时修改模型/模式/选项 |
|
||
| `modeId`, `thinkingOptionId`, `featureValues` | 特定于 Provider 的选择 |
|
||
| `title`, `systemPrompt` | Agent 配置 |
|
||
| `providerOptions` | 不透明的、特定于 Provider 的选项 |
|
||
| `mcpServers`, `toolPolicy` | MCP 配置和精确的工具预批准 |
|
||
| `cwd` | 不能修改 |
|
||
|
||
### 修改已保存的配置 {#change-saved-configuration}
|
||
|
||
在 `contribute(server)` 中添加这个回调,为新的 Codex Agent 设置已保存的选项:
|
||
|
||
```ts
|
||
server.addHook("agent.create.before", async ({ request }) => {
|
||
if (request.config.provider !== "codex") {
|
||
return request;
|
||
}
|
||
|
||
return {
|
||
...request,
|
||
config: {
|
||
...request.config,
|
||
providerOptions: {
|
||
...request.config.providerOptions,
|
||
sandbox_mode: "workspace-write",
|
||
approval_policy: "on-request",
|
||
},
|
||
},
|
||
};
|
||
});
|
||
```
|
||
|
||
| 输入 | 结果 |
|
||
| ------------------------------------------- | ------------------- |
|
||
| `providerOptions.sandbox_mode: "read-only"` | `"workspace-write"` |
|
||
| `providerOptions.web_search: "disabled"` | 由展开运算保留 |
|
||
|
||
所选的 Provider 会校验 `providerOptions`。显式的 Codex 沙盒和批准选项会覆盖它的模式预设。
|
||
|
||
## agent.create.after {#agentcreateafter}
|
||
|
||
常规创建完成后触发,收到 `{ agent }`;导入和恢复不会触发。
|
||
|
||
```ts
|
||
server.addHook("agent.create.after", async ({ agent }) => {
|
||
console.log(agent);
|
||
});
|
||
```
|
||
|
||
## agent.open.before {#agentopenbefore}
|
||
|
||
收到 `{ request }`,只能修改 `env`、`tools` 和 `mcpServers`,修改只用于此次启动。其他字段只读。抛出错误、无效返回或超时会让打开操作失败。
|
||
|
||
**`agent.open.before` 请求示例:**
|
||
|
||
```json
|
||
{
|
||
"agentId": "agent-123",
|
||
"workspaceId": "workspace-456",
|
||
"provider": "codex",
|
||
"cwd": "/projects/shop",
|
||
"title": "Workspace notes",
|
||
"labels": { "notes/enabled": "true" },
|
||
"tools": [],
|
||
"mcpServers": {},
|
||
"reason": "resume",
|
||
"purpose": "interactive",
|
||
"env": { "COMPANY_ENV": "development" }
|
||
}
|
||
```
|
||
|
||
| 字段 | 取值 |
|
||
| ------------- | -------------------------------------------------------------------------------------------------------- |
|
||
| `workspaceId` | 字符串或 `null` |
|
||
| `title` | 只读字符串或 `null`;创建时使用创建请求的标题,恢复时使用已保存的标题。 |
|
||
| `labels` | 只读的字符串映射;创建时使用创建请求的标签,恢复时使用已保存的标签。 |
|
||
| `tools` | 可编辑的 `AgentToolDefinition[]`,仅包含此插件的工具。参阅[为 Agent 附加工具](#attach-tools-to-agents)。 |
|
||
| `mcpServers` | 可编辑的服务器映射,仅用于启动且仅包含本插件的条目。参阅[注入 MCP 服务器](#inject-mcp-servers)。 |
|
||
| `reason` | `create`、`resume`、`refresh`、`import` |
|
||
| `purpose` | `interactive`、`history` |
|
||
| `env` | 启动覆盖映射;不包括 daemon 继承的环境变量。替换整个映射即可添加、替换或移除覆盖项。 |
|
||
|
||
### 在每次打开会话时注入环境变量 {#inject-environment-variables-on-every-session-opening}
|
||
|
||
```ts
|
||
server.addHook("agent.open.before", async ({ request }) => {
|
||
return {
|
||
...request,
|
||
env: {
|
||
...request.env,
|
||
COMPANY_ENV: "development",
|
||
},
|
||
};
|
||
});
|
||
```
|
||
|
||
会话打开时的环境变量覆盖项会在每次打开时重新计算。`agent.create.before.env` 只作用于首次启动,不会写入 Agent 的已保存配置。
|
||
|
||
### 注入 MCP 服务器 {#inject-mcp-servers}
|
||
|
||
需要将服务器保存为 Agent 配置的一部分时,使用 `agent.create.before.config.mcpServers`。
|
||
插件拥有的服务器使用下面的回调,在 `contribute(server)` 中添加,并把占位 URL 替换为你的 MCP 端点:
|
||
|
||
```ts
|
||
server.addHook("agent.open.before", async ({ request }) => {
|
||
if (request.provider !== "codex") return;
|
||
|
||
return {
|
||
...request,
|
||
mcpServers: {
|
||
...request.mcpServers,
|
||
company: { type: "http", url: "https://tools.example.com/mcp" },
|
||
},
|
||
};
|
||
});
|
||
```
|
||
|
||
`request.mcpServers` 使用与 `AgentSessionConfig.mcpServers` 相同的服务器配置。
|
||
每个插件的映射从空开始,只在该插件自己的回调之间累积,不包含 Agent 已保存的服务器或其他插件的服务器。
|
||
替换映射即可添加、替换或移除本插件的条目;返回 `undefined` 会保持不变。
|
||
Paseo 按插件 ID 顺序合并各插件的最终映射。
|
||
|
||
名称与已保存的服务器、Paseo 内部的 `paseo` 服务器或其他插件的服务器冲突时,会话打开失败,错误会指出服务器名称及双方来源。
|
||
不支持 MCP 的 Provider 会报 `does not support MCP servers` 错误,并指出插件名称。
|
||
`purpose: "history"` 的会话打开会忽略这些服务器。安装、停用和重新加载插件后,现有 Agent
|
||
通过[会话重新打开行为](#attach-tools-to-agents)获取变化。
|
||
|
||
### 为 Agent 附加工具 {#attach-tools-to-agents}
|
||
|
||
在 `index.server.ts` 中定义工具,并在 `server.addHook("agent.open.before", async handler)` 中附加它。
|
||
此示例只向带有 `notes/enabled: "true"` 标签的 Agent 提供 `append_workspace_note`:
|
||
|
||
```ts
|
||
import { writeFile } from "node:fs/promises";
|
||
import path from "node:path";
|
||
import { z } from "zod";
|
||
import { defineAgentTool, type PluginServerContext } from "@getpaseo/plugin/server";
|
||
|
||
const appendWorkspaceNote = defineAgentTool({
|
||
name: "append_workspace_note",
|
||
description: "Append a note to AGENT_NOTES.md in this agent's working directory.",
|
||
input: z.object({ note: z.string().min(1).describe("Note to append") }),
|
||
async execute({ note }, { agent, signal }) {
|
||
const notePath = path.join(agent.cwd, "AGENT_NOTES.md");
|
||
await writeFile(notePath, `${note}\n`, { encoding: "utf8", flag: "a", signal });
|
||
return {
|
||
text: `Saved note to ${notePath}`,
|
||
structuredContent: { path: notePath },
|
||
};
|
||
},
|
||
});
|
||
|
||
export default function contribute(server: PluginServerContext) {
|
||
server.addHook("agent.open.before", async ({ request }) => {
|
||
if (request.labels["notes/enabled"] !== "true") return;
|
||
return { ...request, tools: [...request.tools, appendWorkspaceNote] };
|
||
});
|
||
return () => {};
|
||
}
|
||
```
|
||
|
||
通过 SDK 创建 Agent 时设置标签:
|
||
|
||
```ts
|
||
const agent = await paseo.agents.create({
|
||
config: { provider: "codex/gpt-5.4" },
|
||
cwd: "/projects/shop",
|
||
labels: { "notes/enabled": "true" },
|
||
});
|
||
```
|
||
|
||
使用会话打开请求中的字段选择 Agent:
|
||
|
||
| 选择依据 | 回调中的条件 |
|
||
| ---------- | -------------------------------------------- |
|
||
| 标签 | `request.labels["notes/enabled"] === "true"` |
|
||
| Provider | `request.provider === "codex"` |
|
||
| 工作区 | `request.workspaceId === "workspace-456"` |
|
||
| 单个 Agent | `request.agentId === chosenAgentId` |
|
||
|
||
每个插件的 `request.tools` 都从空数组开始。同一插件中的后续回调会看到前面回调返回的工具;
|
||
替换数组即可添加或移除工具。返回 `undefined` 会保留请求。`purpose: "history"` 时会忽略工具。
|
||
|
||
从 `@getpaseo/plugin/server` 导入 `defineAgentTool`、`AgentToolDefinition`、`AgentToolContext`
|
||
和 `AgentToolResult`。辅助函数会根据 Zod schema 推断验证后的输入类型:
|
||
|
||
```ts
|
||
declare function defineAgentTool<Input extends z.ZodType>(tool: {
|
||
name: string;
|
||
title?: string;
|
||
description: string;
|
||
input: Input;
|
||
execute(
|
||
input: z.output<Input>,
|
||
context: AgentToolContext,
|
||
): AgentToolResult | Promise<AgentToolResult>;
|
||
}): AgentToolDefinition;
|
||
|
||
interface AgentToolResult {
|
||
text?: string;
|
||
structuredContent?: unknown;
|
||
isError?: boolean;
|
||
}
|
||
```
|
||
|
||
| 执行 context | 值 |
|
||
| ------------ | --------------------------------------------------------------------------------------------------- |
|
||
| `agent` | 发起调用的 Agent,以 `PluginHookAgent` 表示,包括 `id`、`workspaceId`、`provider`、`cwd` 和 `title` |
|
||
| `paseo` | 已连接到此 daemon 的 SDK |
|
||
| `signal` | 此次工具调用的中止信号;请把它传给文件操作或外部请求 |
|
||
|
||
`execute` 在插件进程中运行。闭包和原始 Zod schema 保留在该进程中;只有工具元数据和调用的
|
||
输入、结果会跨进程传递。Paseo 在调用 `execute` 前验证输入。无效输入会将 schema 问题返回给
|
||
Agent。抛出的错误会转换为 `{ isError: true, text: message }`,插件继续运行。取消调用或关闭
|
||
其传输连接会中止 `signal` 并丢弃结果。工具调用最长运行 10 分钟,与钩子的 30 秒超时分开。
|
||
关闭或重新打开 Agent 会话会移除旧定义。
|
||
|
||
在 `text` 中返回模型可见的文本。`structuredContent` 必须可序列化为 JSON;省略 `text` 时,
|
||
Paseo 会将该值格式化为供模型读取的文本。设置 `isError: true` 可报告工具错误。
|
||
|
||
Paseo 通过 Agent 工具传输提供附加的工具,按 Provider 的支持情况使用 MCP 或原生注册。
|
||
同一份定义和回调适用于这两种方式。附加的工具遵循[与 Paseo 工具相同的限制](/docs/mcp#limit-paseo-tools-by-provider)。
|
||
只有该 Agent 能看到这些工具。
|
||
|
||
名称必须匹配 `/^[a-z][a-z0-9_]*$/`,且不超过 64 个字符。如果名称与同一 Agent 的 Paseo
|
||
工具或另一插件的工具冲突,会话打开会失败,并指出双方。一个插件内的重复名称也会导致
|
||
会话打开失败。回调未选中的 Agent 不受影响。
|
||
|
||
| 变更 | Agent 何时收到变更 |
|
||
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| 启动、停止或重新加载声明了 `agent.open.before` 的插件 | 已打开的会话会标记为过期。每个会话在下一次空闲提示词前重新打开;正在运行的轮次会继续,不会被中断。重新打开后的下一条提示词复用该会话。 |
|
||
| 修改回调选择的 Agent | 编辑此示例后运行 `paseo plugin reload agent-tools`,然后在 Agent 空闲时发送提示词。 |
|
||
| 立即应用 | 调用 `await paseo.agents.ref(id).reload({ requestId: "reload-notes" })`。`requestId` 可选。这会重新打开 Provider 会话,以 `reason: "refresh"` 再次运行钩子,并中断正在运行的轮次。 |
|
||
| 读取当前快照 | 调用 `await paseo.agents.ref(id).refresh()`。这只读取状态,不会重新打开会话。 |
|
||
|
||
如果自动重新打开失败,触发它的提示词会返回该错误。参阅
|
||
[完整 agent-tools 示例](https://github.com/getpaseo/paseo/tree/main/plugin-examples/agent-tools)。
|
||
|
||
## agent.close.after {#agentcloseafter}
|
||
|
||
运行中的 Agent 实例关闭后触发,收到 `{ agent }`。
|
||
|
||
```ts
|
||
server.addHook("agent.close.after", async ({ agent }) => {
|
||
console.log(agent);
|
||
});
|
||
```
|
||
|
||
## agent.archive.after {#agentarchiveafter}
|
||
|
||
归档状态保存后触发,收到 `{ agent, archivedAt }`;运行时清理可能尚未完成。
|
||
|
||
```ts
|
||
server.addHook("agent.archive.after", async ({ agent, archivedAt }) => {
|
||
console.log(agent, archivedAt);
|
||
});
|
||
```
|
||
|
||
## agent.turn.before {#agentturnbefore}
|
||
|
||
实时轮次开始后触发,收到 `{ agent, turnId }`。历史重放不会触发。
|
||
|
||
```ts
|
||
server.addHook("agent.turn.before", async ({ agent, turnId }) => {
|
||
console.log(agent, turnId);
|
||
});
|
||
```
|
||
|
||
## agent.turn.after {#agentturnafter}
|
||
|
||
实时轮次完成、失败或取消后触发,收到 `{ agent, turnId, outcome, timeline }`。`outcome.kind` 为 `completed`、`failed` 或 `canceled`。`timeline` 已弃用且始终为空;请按下面的示例通过 SDK 获取历史。
|
||
|
||
### 在轮次结束时发送后续消息 {#send-a-follow-up-when-a-turn-ends}
|
||
|
||
把 [server/inspect.ts](https://github.com/getpaseo/paseo/blob/main/plugin-examples/lifecycle-actions/server/inspect.ts) 复制到你的插件中。该辅助文件从 `@getpaseo/protocol/agent-types` 导入类型;请把与 daemon 版本相同的 `@getpaseo/protocol` 加入开发依赖,并在加载插件之前安装它们。`latestOutputText` 会拼接最近一条用户消息之后的文本片段。
|
||
|
||
```ts
|
||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||
import { latestOutputText } from "./server/inspect";
|
||
|
||
export default function contribute(server: PluginServerContext) {
|
||
server.addHook("agent.turn.after", async (event, context) => {
|
||
if (event.outcome.kind === "canceled") {
|
||
return;
|
||
}
|
||
|
||
const agent = context.paseo.agents.ref(event.agent.id);
|
||
const page = await agent.timeline.refetch({ direction: "tail", limit: 20 });
|
||
const text = latestOutputText(page.entries.map(({ item }) => item));
|
||
if (/out of credits/i.test(text)) {
|
||
await agent.send("Try again.");
|
||
}
|
||
});
|
||
|
||
return () => {};
|
||
}
|
||
```
|
||
|
||
```text
|
||
Turn ends: "out of credits"
|
||
→ plugin sends "Try again."
|
||
→ a new turn starts
|
||
```
|
||
|
||
该处理函数只获取最近 20 条时间线条目,不检查这一页之前的输出。插件需要更多历史时,请用 `cursor` 和 `direction` 继续翻页。
|
||
|
||
这会用现有的 SDK 发送一条新消息。持续匹配会不断发送后续消息;需要时请在插件中添加次数限制或延迟。附件和工具的效果不会被重放。
|
||
|
||
## agent.permission.before {#agentpermissionbefore}
|
||
|
||
权限或问题进入待处理状态后触发,收到 `{ agent, request }`;`request` 为 `AgentPermissionRequest`。通过 SDK 回应。
|
||
|
||
### 回应权限请求 {#answer-a-permission-request}
|
||
|
||
使用同一个[辅助文件](https://github.com/getpaseo/paseo/blob/main/plugin-examples/lifecycle-actions/server/inspect.ts)中的 `shellCommand`:
|
||
|
||
```ts
|
||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||
import { shellCommand } from "./server/inspect";
|
||
|
||
export default function contribute(server: PluginServerContext) {
|
||
server.addHook("agent.permission.before", async (event, context) => {
|
||
const command = shellCommand(event.request);
|
||
if (command === null) {
|
||
return;
|
||
}
|
||
|
||
const agent = context.paseo.agents.ref(event.agent.id);
|
||
if (/\brm\s+-rf\b/.test(command)) {
|
||
await agent.respondToPermission({
|
||
requestId: event.request.id,
|
||
response: { behavior: "deny", message: "Recursive deletion is blocked." },
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (command.trim() === "git status") {
|
||
await agent.respondToPermission({
|
||
requestId: event.request.id,
|
||
response: { behavior: "allow" },
|
||
});
|
||
}
|
||
});
|
||
|
||
return () => {};
|
||
}
|
||
```
|
||
|
||
| 请求 | 结果 |
|
||
| ---------------- | -------------------- |
|
||
| `rm -rf build` | 拒绝 |
|
||
| `git status` | 批准 |
|
||
| 其他命令或请求 | 保持待处理,留给用户 |
|
||
| 已经处理过的请求 | SDK 响应失败 |
|
||
|
||
这个正则表达式只是一个示例策略,而不是 shell 解析器。权限请求也可能是问题、计划和模式变更;请求权限不会结束当前轮次。
|
||
|
||
## agent.permission.after {#agentpermissionafter}
|
||
|
||
待处理请求得到回应或清除后触发,收到 `{ agent, requestId, resolution }`;`resolution` 为 `AgentPermissionResponse`。
|
||
|
||
```ts
|
||
server.addHook("agent.permission.after", async ({ agent, requestId, resolution }) => {
|
||
console.log(agent, requestId, resolution);
|
||
});
|
||
```
|
||
|
||
Agent 钩子不包括 daemon 的私有临时辅助 Agent。后台 Agent 和后台工作区中的 Agent 使用相同钩子。
|
||
|
||
## 顺序与返回值 {#ordering-and-returned-values}
|
||
|
||
```text
|
||
Creation request
|
||
→ agent.create.before hooks (plugin-ID order; registration order within each plugin)
|
||
→ resolve defaults and validate provider configuration
|
||
→ derive launch configuration with Paseo runtime tools and daemon prompt
|
||
→ agent.open.before hooks (same ordering; env and plugin-local tools and MCP servers)
|
||
→ set PASEO_AGENT_ID and PASEO_AGENT_CWD
|
||
→ open provider session and save agent configuration
|
||
```
|
||
|
||
| 回调返回 | 下一个回调收到 |
|
||
| ------------------------------------------------------- | --------------------------------- |
|
||
| `{ ...request, env: { ...request.env, REGION: "eu" } }` | 之前的请求,添加或替换了 `REGION` |
|
||
| `{ ...request, env: { REGION: "eu" } }` | 之前的请求,整个覆盖映射被替换 |
|
||
| `undefined` | 未改变的请求 |
|
||
| 抛出错误或返回无效数据 | 操作失败;后面的回调不会运行 |
|
||
|
||
没有自动的深度合并。`env` 在插件之间累积;`tools` 和 `mcpServers` 只在同一插件的回调之间累积。
|
||
返回替换的映射或数组会替换该范围内之前的值。
|
||
|
||
## context 与清理 {#context-and-cleanup}
|
||
|
||
| 契约 | 行为 |
|
||
| -------------------------- | ---------------------------------------------------------------------------------- |
|
||
| `context.paseo` | 已连接到此 daemon 的现有 SDK |
|
||
| `context.signal` | 在调用超时或插件停止时中止;请把它传给外部请求 |
|
||
| 输入数据 | 分离的快照;通过返回的请求或 SDK 命令改变状态 |
|
||
| 注册结果 | 幂等的移除函数,例如 `const remove = server.addHook(...); remove();` |
|
||
| 重新加载、停用、移除、关闭 | 移除剩余的注册 |
|
||
| 未知的钩子名称 | 注册失败 |
|
||
| 钩子超时 | 30 秒;会中止 signal。等待型钩子会让待处理的操作失败;事件处理函数会记录一条错误。 |
|
||
| 事件处理函数出错 | 记录在插件名下;原操作继续进行 |
|
||
| 事件投递 | 实时、尽力而为;没有重放、持久化或自动重试 |
|
||
| 事件并发 | 不同的事件可能重叠;不保证回调完成的顺序 |
|
||
|
||
## 完整示例 {#complete-examples}
|
||
|
||
| 插件 | 包含 |
|
||
| ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- |
|
||
| [lifecycle-logger](https://github.com/getpaseo/paseo/tree/main/plugin-examples/lifecycle-logger) | Agent、工作区和 worktree 钩子;隐去环境变量值的 JSON 日志 |
|
||
| [lifecycle-actions](https://github.com/getpaseo/paseo/tree/main/plugin-examples/lifecycle-actions) | 后续消息、权限、环境变量、切换 Provider、选择 worktree |
|
||
| [agent-configuration](https://github.com/getpaseo/paseo/tree/main/plugin-examples/agent-configuration) | MCP 注入以及 Codex 沙盒/批准选项 |
|
||
| [agent-tools](https://github.com/getpaseo/paseo/tree/main/plugin-examples/agent-tools) | 按标签选择 Agent,并在插件进程中执行文件操作的工具 |
|
||
|
||
用 `paseo plugin logs lifecycle-logger` 或主机的 `daemon.log` 读取 logger 的输出。
|
||
|
||
## 已重命名的钩子 {#renamed-hooks}
|
||
|
||
旧注册方法已弃用,会在 2027-04-30 之后移除。现有插件继续按原来的方式运行。同一插件中混合使用新旧注册方法时,处理函数按注册顺序运行。`addHook` 只接受新名称。
|
||
|
||
| 旧注册方式 | 新注册方式 |
|
||
| -------------------------------------------------- | --------------------------------------------------------- |
|
||
| `server.before("workspace.create", handler)` | `server.addHook("workspace.create.before", asyncHandler)` |
|
||
| `server.on("workspace.created", handler)` | `server.addHook("workspace.create.after", asyncHandler)` |
|
||
| `server.on("workspace.archived", handler)` | `server.addHook("workspace.archive.after", asyncHandler)` |
|
||
| `server.before("agent.create", handler)` | `server.addHook("agent.create.before", asyncHandler)` |
|
||
| `server.on("agent.created", handler)` | `server.addHook("agent.create.after", asyncHandler)` |
|
||
| `server.before("agent.session_open", handler)` | `server.addHook("agent.open.before", asyncHandler)` |
|
||
| `server.on("agent.closed", handler)` | `server.addHook("agent.close.after", asyncHandler)` |
|
||
| `server.on("agent.archived", handler)` | `server.addHook("agent.archive.after", asyncHandler)` |
|
||
| `server.on("agent.turn_started", handler)` | `server.addHook("agent.turn.before", asyncHandler)` |
|
||
| `server.on("agent.turn_ended", handler)` | `server.addHook("agent.turn.after", asyncHandler)` |
|
||
| `server.on("agent.permission_requested", handler)` | `server.addHook("agent.permission.before", asyncHandler)` |
|
||
| `server.on("agent.permission_resolved", handler)` | `server.addHook("agent.permission.after", asyncHandler)` |
|