跳到主要内容

数据截至 (上游 commit 60706feb348c)

让 agent 指挥 agent:工具目录、MCP、定时与循环

30 秒导读: 前面几章讲的是「人怎么通过 Paseo 控制 agent」。这一章讲的是 Paseo 的另一半野心: 把这套控制面反过来交给 agent 自己用。守护进程把「建工作区、开 agent、发提示、看状态、开终端、下定时任务」 打包成 39 个工具,通过两条投递路径塞进正在跑的 agent 里。于是一个 agent 可以像人一样点开另一个 agent。


1. 这是什么(零基础也能懂)

一句话定义: Paseo 守护进程把「自己能做的每一件事」注册成一份工具清单,让运行中的 agent 直接调用。

它解决什么问题。 你在终端里让 Claude 干活,它做到一半发现:这个任务应该拆成三份并行做;或者 「我改完了,得有人复查」;或者「这个 PR 要每五分钟看一眼 CI」。

传统做法是回来点三次。Paseo 的做法是:agent 自己调 create_agent 开三个子 agent、调 create_heartbeat 挂一个定时唤醒、调 send_agent_prompt 去催那个复查的 agent。

谁在用。 三类调用者共用同一份工具清单:

调用者怎么接进来典型动作
运行中的 agent工具被注入进它的会话开子 agent、发提示、挂心跳
外部 MCP 客户端连守护进程的 /mcp/agents HTTP 端点脚本化批量管理 agent
paseo CLI / 手机 App走 WebSocket 协议(见 01)人手动操作

用起来什么样。 用户对 agent 说一句 "committee this",agent 读仓库里的技能说明,然后自己发出工具调用:

// 示意,非源码 —— agent 发出的一次工具调用
{
"name": "create_agent",
"arguments": {
"title": "Root-cause: flaky auth test",
"provider": "codex/gpt-5.4", // 必须是 provider/model 形式
"initialPrompt": "分析 auth 测试为何间歇失败……(自包含简报)",
"notifyOnFinish": true // agent 之间调用时默认就是 true
}
}

notifyOnFinish: true 是这套东西能闭环的关键:子 agent 干完活,守护进程会主动给父 agent 发一条系统提示, 父 agent 因此被唤醒继续往下走——不需要它轮询。

一句话直觉。 把守护进程想成一台机器的「操作系统」,agent 是进程。这一章讲的就是 Paseo 给进程开的 系统调用表:fork(create_agent)、killsignal(send_agent_prompt)、cron(create_schedule)。


2. 顶层全景(它大概怎么转)

怎么读这张图: 中间那个盒子是唯一的真相——一份工具目录。左边是它的两个投递出口,右边是它背后真正干活的服务。

┌──────────────── 两条投递路径 ────────────────┐
│ │
① 原生注入(provider 直接吃) ② MCP 适配(标准协议)
AgentLaunchContext.paseoTools HTTP /mcp/agents
│ │
└──────────────┬───────────────────────────────┘

┌────────────────────────────┐
│ PaseoToolCatalog │ ← 唯一真相
│ createPaseoToolCatalog() │
│ 39 个工具 · zod 校验 │
└─────────────┬──────────────┘
│ 每个 handler 调下面某个服务
┌──────────┬──────────┼──────────┬───────────┐
▼ ▼ ▼ ▼ ▼
AgentManager Workspace Terminal ScheduleService Voice
生命周期 工作区 终端 定时/心跳 speak

部件一句话职责:

部件干什么在哪个文件
PaseoToolCatalog工具的注册表 + 分发器packages/server/src/server/agent/tools/types.ts:27
createPaseoToolCatalog造目录:注册全部工具、绑好依赖packages/server/src/server/agent/tools/paseo-tools.ts:541
原生注入路把目录塞进 provider 的「宿主工具」通道packages/server/src/server/agent/providers/omp/host-tools.ts:33
MCP 适配路把目录薄封装成一个 MCP serverpackages/server/src/server/agent/mcp-server.ts:31
ScheduleServicecron 定时 agent 与心跳packages/server/src/server/schedule/service.ts:244

(注:旧版的两个编排部件——LoopService(worker/verifier 迭代循环)与 FileBackedChatService(agent 聊天室)——已在本 commit 移除,见 §5.2/§5.4 的说明。)

主线走一遍(高层):

  1. 守护进程启动时把目录工厂交给 AgentManager(bootstrap.ts:1377,setPaseoToolCatalogFactory)。
  2. 某个 agent 要启动 → AgentManager 现场为它造一份目录,目录里记着「调用者是谁」。
  3. 目录按 provider 能力走 ① 或 ② 投给 agent 进程。
  4. agent 调用 create_agent → 目录的 handler 调 AgentManager → 新 agent 起来。
  5. 新 agent 结束 → 完成通知作为一条提示回灌给父 agent → 父 agent 继续。

3. 工具目录:一份清单,两个消费者

3.1 类型骨架先看懂

目录只有五个类型,读完就知道全貌(agent/tools/types.ts):

类型是什么
PaseoToolDefinition一个工具:名字 + 描述 + zod 入参 + handlertypes.ts:21
PaseoToolCatalog一张只读 Map + getTool + executeTooltypes.ts:27
PaseoToolExecutionContext调用期上下文:signal 取消、sendUpdate 流式中间结果types.ts:3
PaseoToolRuntimeContext造目录时的身份:callerAgentIdenableVoiceToolsvoiceOnlytypes.ts:37
PaseoToolCatalogFactory(runtimeContext) => Catalog —— 目录是按调用者现造的types.ts:43

最后一条是整个设计的支点。目录不是全局单例,而是每个调用者一份。因为「谁在调用」会改变工具的 schema 和默认值(见 3.4)。

3.2 注册与分发:三十行搞定

createPaseoToolCatalog 内部只有一个 Map 和一个闭包 registerTool(paseo-tools.ts:573-587), 所有工具都往这个 Map 里塞。分发同样朴素(paseo-tools.ts:593-603):

const tool = tools.get(name);
if (!tool) {
throw new Error(`Paseo tool not found: ${name}`);
}
return tool.handler(await parseToolInput(tool, input), context);

重点看 parseToolInput(paseo-tools.ts:558-570):它在 handler 之前做 zod 校验,并且能吃两种 schema 形态—— 完整的 z.ZodType,或者一堆字段组成的 ZodRawShape(后者会被包成 z.object(...).passthrough())。

passthrough() 是刻意的:模型经常多塞字段,直接 strict 会把整次调用打回,不如放行未知字段。

3.3 39 个工具,分成六族

工具族工具干什么
工作区create_workspace list_workspaces archive_workspace rename_workspace开/关并行开发底座(见 05)
agent 生命周期create_agent send_agent_prompt get_agent_status list_agents cancel_agent archive_agent kill_agent update_agent set_agent_mode get_agent_activity指挥另一个 agent(见 03)
终端list_terminals create_terminal capture_terminal send_terminal_keys kill_terminal开一个真终端并读回屏幕
工作区脚本list_workspace_scripts start_workspace_script stop_workspace_script启停 paseo.json 里配置的服务
定时create_schedule create_heartbeat delete_heartbeat list_schedules inspect_schedule pause_schedule resume_schedule delete_schedule update_schedule schedule_logs run_schedule_once让未来的自己/别人被唤醒
其它list_providers list_models inspect_provider list_pending_permissions respond_to_permission speak发现能力、代批权限、说话

另有一族浏览器工具(browser_clickbrowser_snapshot 等 12 个)只在开关打开时挂载, 由 registerBrowserTools 注入同一个 registerTool(paseo-tools.ts:1202-1209)。

speak 是特例:它只在语音开关打开时注册,并且在 voiceOnly 模式下目录到此为止直接返回 (paseo-tools.ts:1198-1200)——语音 agent 只有一个工具,不给它开 agent 的权力。语音本身的实现 在 server/session/voice/voice-session.ts:1031(registerVoiceBridgeForAgent)把 handler 挂上来, 本章不展开。

3.4 调用者身份会改写 schema

同一个工具名,在「agent 调用」和「外部客户端调用」两种场景下,入参 schema 和默认值不一样。 以 send_agent_prompt 为例(paseo-tools.ts:1139-1141):

参数agent 调用时默认外部调用时默认为什么
backgroundtruefalseagent 不该阻塞在等待里,它有别的活干
notifyOnFinishtruefalseagent 需要被回调唤醒;脚本不需要

create_agent 更进一步:agent 调用时没有 background 参数,而外部调用时没有「默认继承调用者工作区」 这层行为(paseo-tools.ts:1011-1039)。判据就一个字段——callerAgentId 是否存在。

cwd 也被同一个身份约束住:resolveScopedCwd(paseo-tools.ts:649-669)在 agent 场景下强制从 父 agent 的 cwd 派生,并尊重 lockedCwd / allowCustomCwd;没有调用者身份时则必须显式传 cwd。

3.5 结果要让模型「看得见」

工具返回 { content, structuredContent } 两份数据。问题是:很多 handler 只填 structuredContent (结构化 JSON),content(模型可见文本)是空的——模型于是什么都读不到。

addModelVisibleStructuredContent(agent/tools/paseo-tool-serialization.ts:50)补这一刀: 当 structuredContent 有值而 content 为空时,把 JSON 渲染成文本塞进 content

渲染时还做了一个小优化(paseo-tool-serialization.ts:18,formatStructuredContentForModel): 遇到数组字段就先摘一行 agents_count=3 / agents_ids=a,b,c 的摘要放在完整 JSON 前面。 模型读第一行就能抓到 id,不必啃完整个 JSON。


4. 两条投递路径:同一份目录,两种运法

4.1 为什么要两条

provider 是异构的(见 02)。有的 CLI 支持标准 MCP,有的有自己的 「宿主工具」私有通道。Paseo 不想为此写两份工具,于是把目录投递拆开。

PaseoToolCatalog(一份)

┌───────────────────┴────────────────────┐
│ │
supportsNativePaseoTools = true 其它 provider
│ │
┌────────▼─────────┐ ┌─────────▼────────┐
│ 塞进 launchContext │ │ 注入一条 mcpServers │
│ .paseoTools │ │ 配置指向本机 HTTP │
└────────┬─────────┘ └─────────┬────────┘
│ │
provider 进程内直接持有目录 agent 进程作为 MCP 客户端回连
│ │
setOmpHostTools() 下发定义 GET/POST /mcp/agents
│ │
host_tool_call 事件回来 标准 MCP tools/call
│ │
└──────────► catalog.executeTool() ◄─────┘

4.2 两路对比

维度① 原生注入② MCP 适配
触发条件provider capabilities.supportsNativePaseoTools === true其余全部
目录怎么到达AgentLaunchContext.paseoTools(内存对象)HTTP URL + Bearer token 写进会话配置
入参 schema 怎么变serializePaseoToolInputParameters 手动转 JSON Schema直接把 zod 交给 MCP SDK 的 registerTool
进程边界同进程,零序列化开销跨进程,走本机 HTTP
支持取消支持(host_tool_cancelAbortController)支持(MCP 请求的 signal)
支持流式中间结果支持(sendUpdatehost_tool_update)不支持——mcp-server.ts:47 只传 signal
目录状态会话期常驻每次 HTTP 请求现造一份,响应关闭即销毁

最后一行是个值得记住的取舍。MCP 路走的是无状态模式(sessionIdGenerator: undefined), bootstrap.ts:1389-1394 的注释给了理由:agent 控制面只做「列工具」和「调工具」,没有跨请求状态可留; 留了反而会被那些不干净退出的 agent 永久占住内存。

4.3 MCP 路:一层薄封装 + 一个被特批的路由

createAgentMcpServer(agent/mcp-server.ts:31-52)总共 20 行:造目录、for 循环把每个工具 server.registerTool 一遍、把结果映射成 CallToolResult。它本身不含任何业务逻辑

麻烦的是这条路的鉴权。守护进程可以设密码,但密码在 App 里设置时只存哈希、明文拿不到—— 注入进 agent 配置的那条 MCP 连接因此没法带密码。解法是一枚每次启动随机生成的能力令牌:

  • bootstrap.ts:639 生成 agentMcpAuthToken = randomUUID()
  • runtime-mcp-config.ts:51-53 把它写成注入配置的 Authorization: Bearer …
  • /mcp/agents 被从全局密码中间件里摘出去(auth.ts:145),改由 isAgentMcpRequestAuthorized(auth.ts:163)单独把关:先常量时间比对能力令牌,不中再退回密码校验。

令牌只写进本机 agent 配置、从不发给远程客户端,所以拿不到就没法离机重放。

调用者身份则挂在 query 上:?callerAgentId=<id>(runtime-mcp-config.ts:50), 路由侧解析后传给目录工厂(bootstrap.ts:1458-1466)。

4.4 原生路:把目录翻译成 provider 的宿主工具

只有 omp 这一个 provider 目前声明 supportsNativePaseoTools: true (agent/providers/omp/agent.ts:462-467)。AgentManager 的判据是三个条件同时成立 (agent/agent-manager.ts:4781-4787):

if (
this.paseoToolsEnabled &&
client.capabilities.supportsNativePaseoTools &&
this.paseoToolCatalogFactory
) {
context.paseoTools = await this.paseoToolCatalogFactory({ callerAgentId: agentId });
}

provider 拿到后调 setOmpHostTools(omp/host-tools.ts:48),把目录序列化成宿主工具定义下发; 每个工具都标 loadMode: "essential",意思是不做懒加载、模型一上来就能看见全部 (omp/host-tools.ts:33-46)。

回调侧是一个 OmpHostToolRouter(omp/host-tools.ts:144),按 runtime session 用 WeakMap 缓存。 它比 MCP 路多做三件事:

  1. 取消:每次调用配一个 AbortController,收到 host_tool_cancel 就 abort(host-tools.ts:168-175)。
  2. 迟到结果丢弃:已取消的调用即使 handler later 返回了,也不回传(host-tools.ts:205-212)。
  3. 流式中间结果:handler 通过 sendUpdatehost_tool_update 帧(host-tools.ts:226-233)。

4.5 互斥:注入原生就得摘掉内置 MCP

两条路同时开会让同一批工具在模型眼里出现两次。所以原生注入成功时,内置的那条 MCP 配置要被拆掉 (agent-manager.ts:4791-4796):

return launchContext.paseoTools ? stripInternalPaseoMcpServer(launchConfig) : launchConfig;

stripInternalPaseoMcpServer(runtime-mcp-config.ts:6)删得很克制:只删名字叫 paseo 并且 URL 路径正好是 /mcp/agents 的那一条(isInternalPaseoMcpServer,runtime-mcp-config.ts:60)。 用户自己配的、恰好也叫 paseo 的外部 MCP server 不会被误伤。

另一个细节:注入是运行时行为,不写回持久化配置。withRuntimePaseoMcpServer (runtime-mcp-config.ts:29)先 strip 再加,产出的是一份临时 launch config。 mcp-parity.e2e.test.ts:394-404 正是断言这一点——launch config 里有 mcpServers.paseo, 而落盘快照和内存 agent 里都没有。

4.6 一致性靠什么保证

第一层是结构: 两条路调的是同一个 catalog.executeTool,同一份 zod schema,同一个 addModelVisibleStructuredContent。语义分叉的空间被压到只剩「传不传 sendUpdate」。

第二层是端到端测试: agent/mcp-parity.e2e.test.ts(943 行)起一个真守护进程, 用真 MCP HTTP 客户端连两个端点——匿名的 /mcp/agents 和带 ?callerAgentId= 的—— 再跨五个套件把工具挨个跑一遍:

套件覆盖
A: Core Fixes父子标签、MCP 注入 URL、provider/model 语法、feature 透传
B: Terminal Tools建终端、送键、抓屏
C: Schedule Tools建/查/暂停/恢复/删定时
D: Provider Tools列 provider 与模型
E: Worktree Toolsworktree 工作区的建与归档

测试里的 fake provider 被显式设成 supportsNativePaseoTools: false (对照 agent/agent-mcp.e2e.test.ts:108),把执行强制压到 MCP 那条路上。 原生那条路则由 omp/host-tools.test.ts 单独覆盖(标 essential、路由、取消丢弃三个用例)。


5. 有了工具之后:四种协作机制

工具只是「能按按钮」。真正让多 agent 跑起来的是下面四套机制。

5.1 完成通知:把「等待」翻成「被唤醒」

要解决的小问题: 父 agent 开了子 agent 之后干嘛?轮询状态既烧 token 又占着 turn。

思路: 反过来——子 agent 一有动静,守护进程就往父 agent 的输入里塞一条系统提示。

setupFinishNotification(agent/agent-prompt.ts:385)订阅子 agent 事件,在 「完成 / 出错 / 要权限」三种情形里各触发一次,且只触发一次(fired 标志)。 通知内容含子 agent 的标题和它最后一条助手消息(agent-prompt.ts:415-427), 父 agent 因此不必再去读时间线。

两个防漏电细节:发通知前先查父 agent 是否已归档(归档就不发,agent-prompt.ts:410-413); requireParentOwnership 打开时还要校验子 agent 的 parent 标签确实指向自己。

所以 send_agent_prompt 的 background 返回里会附一句 guidance,明说「别轮询」 (paseo-tools.ts:1937-1939)。

5.2 聊天室与 @提及:多对多的那条边(已移除)

⚠️ 已移除: 旧版 packages/server/src/server/chat/(chat-service.tsFileBackedChatService/parseMentionAgentIds/waitForMessageschat-mentions.tsprepareChatMentionFanout/notifyChatMentions)与 paseo chat CLI 已在本 commit 整体删除。 以下保留旧实现的设计讲解,读代码时别再去找这些文件。

通知是父子之间的单向边。旧版要让平级 agent 互相喊话,Paseo 给过聊天室。

消息落盘时就把 @ 解析出来(parseMentionAgentIds),正则要求 @ 前面是行首或空白/左括号,后面是字母数字开头的标识符。

分发分两步走,拆得很干净:

步骤函数干什么
1. 算名单prepareChatMentionFanout展开 @everyone、剔掉自己、过滤不可达者
2. 真发送notifyChatMentions逐个解析 id、复查资格、发系统提示

为什么要分两步: 名单要先算出来才能拦住扇出爆炸。@everyone 会展开成房间里所有发过言的 agent, 超过 CHAT_MENTION_FANOUT_LIMIT = 25 就直接报错让你收窄。

资格判定(isChatMentionTargetEligible)排除四类目标:自己、内部 agent、 已归档、处于 error 态。注意第二步会再判一次——因为显式 @ 写的可能是自定义标题, 解析成规范 id 之后才知道那个 agent 其实已经归档了。

发出去的通知末尾会附一行 Read the room with: paseo chat read <room> --limit 20 ,等于告诉被叫醒的 agent 下一步该干什么。

5.3 定时 agent 与心跳:同一个引擎,两种 target

ScheduleService 每秒 tick 一次(SCHEDULE_TICK_INTERVAL_MS = 1000,schedule/service.ts:32), 扫所有 active 且到点的 schedule(service.ts:537-556)。区别全在 target 这个联合类型上:

target由哪个工具创建触发时干什么用在什么场景
{ type: "agent", agentId }create_heartbeat这个已有 agent 发一条系统提示「每 5 分钟回来看一眼 CI」——回到同一段对话
{ type: "new-agent", config }create_schedule新建工作区 + 新建 agent + 跑一次 + 收工「每天早上跑一遍依赖巡检」——每次全新上下文

心跳分支的执行(service.ts:828-855)有三道闸:agent 记录不在 → 抛 ScheduleTargetGoneError; 已归档 → 同样抛;已有 in-flight run → 拒绝并发。

新建 agent 分支(service.ts:857-940)要重得多:先建工作区、把 workspaceId 记进 run 记录(为了崩溃恢复)、 打上 paseo.schedule-id / paseo.schedule-run 标签、跑完再按 archiveOnFinish 决定要不要归档工作区。

幂等创建。 两个 create 工具走的都是 createOrReplace(service.ts:333)而非 create。 理由写在注释里(service.ts:330-332):像 babysit-pr 这种技能会反复重新注册同一个心跳, 按「名字 + target」upsert 就地刷新,免得攒出一堆重复定时器。

cron 的实现是暴力扫描。 computeNextRunAt(schedule/cron.ts:88)从下一分钟开始, 一分钟一分钟往前试,最多试 366 * 24 * 60 次(即一年)。时区通过 Intl.DateTimeFormat 逐分钟重算日期分量(cron.ts:34-77)——朴素,但避开了自己实现夏令时的坑。

5.4 Ralph 循环:worker 干、verifier 判、不过就重来(已移除)

⚠️ 已移除: packages/server/src/server/loop-service.ts(LoopService/executeLoop/ runVerification)、paseo loop CLI 与 skills/paseo-loop 技能已在本 commit 删除。 以下保留旧实现的设计讲解;「定时起 agent」的诉求仍由 §5.3 的 ScheduleService 承担, 新增的 skills/paseo-plugin 技能(见 §6)接管了原 loop 技能的位置。

要解决的小问题(旧): 「一直试到测试通过为止」这种任务,模型自己在一个 turn 里做不了—— 上下文会爆,而且它没法客观判断自己成没成。

思路: 每次迭代换一批全新的 agent,再用一个独立的 verifier(甚至换一家 provider)来判。

第 N 轮
├─ 建 worker agent(internal,不出现在列表里)
├─ 跑 prompt ────────────────► 失败 ─┐
├─ 成功 → 跑 verifyChecks(shell)──► 失败 ─┤
├─ → 跑 verifyPrompt(agent)──► 失败 ─┤
│ 全过 → 收工 │
└─ 清理 agent ◄────────────────────────────┘
│ sleep(可选)

第 N+1 轮

主循环在 LoopService.executeLoop,是一个没有上界的 for,靠三个条件退出:maxIterationsmaxTimeMs 截止、以及 signal.aborted

验证是两级的(runVerification):

级别形态何时用失败后
verifyChecksshell 命令数组,逐条跑客观判据(npm testgh pr checks)第一条挂就返回 false,不再跑 agent
verifyPrompt起一个 verifier agent 问一句需要判断力(「改动连贯吗」)记下 reason 进迭代记录

先跑 shell 再跑 agent 的顺序是省钱:能被命令否掉的失败,不必花一次模型调用。

每次迭代都是干净的。 worker 和 verifier 都以 internal: true 创建,不进普通列表、不触发通知;跑完按 loop.archive 决定归档还是关闭。所以第 N+1 轮的 worker 完全不知道第 N 轮试过什么—— 上下文不会累积,代价是经验也不会累积。

过程可观测。 循环订阅 agent 的流事件,把每条转成日志写进 LoopRecord.logs,paseo loop logs <id> 读的就是它。

5.5 让 agent 返回可解析的 JSON:三层容错

凡是要 agent 回结构化 JSON 的调用方(旧版 Ralph 循环的 verifier 要 { passed, reason })都会遇到同一件事:模型回的常常是「一段解释 + 一个 ```json 块」。 agent/agent-response-loop.ts 用三层往回捞:

第一层,提示里就给 schema。 buildStructuredAgentResponsePrompt(agent-response-loop.ts:185) 把 zod schema 转成 JSON Schema 附在提示后面。

第二层,从散文里抠 JSON。 extractJsonFromMarkdown(agent-response-loop.ts:206)先试围栏块, 再退回 extractFirstJsonSnippet —— 后者扫出所有 { / [ 的位置,对每个位置做带引号感知的括号配平 (extractBalancedJsonCandidate,agent-response-loop.ts:229),配平后立刻 JSON.parse 验一遍, 成了才认。所以模型在 JSON 前后加废话不会打断流程。

第三层,带错误重问。 校验失败就把具体错误拼进下一轮提示(buildRetryPrompt,agent-response-loop.ts:194), 默认重试 2 次(getStructuredAgentResponse,agent-response-loop.ts:307-352),仍不行才抛 StructuredAgentResponseError,并把最后一次原始回答和全部校验错误挂在异常上 (agent-response-loop.ts:348-351)——方便日志里定位到底是模型问题还是 schema 太苛刻。

重试提示长这样:

<原始提示 + JSON Schema>

Previous response was invalid with validation errors:
- passed: Expected boolean, received string
- reason: String must contain at least 1 character(s)

Respond again with JSON only that matches the schema.

校验器同时支持 zod 和裸 JSON Schema 两种入口(后者走 Ajv),由 buildValidator 分派 (agent-response-loop.ts:165-173)。


6. 落到用户:技能与 CLI

上面所有机制,用户不会直接调工具。仓库自带四个技能文件,把编排套路写成给 agent 读的说明书 (skills/*/SKILL.md):

技能编排形状底层用到什么
paseo-handoff把当前任务连同上下文整体交给一个新 agentcreate_agent + 自包含简报
paseo-advisor单个 agent 出第二意见,不接手工作create_agent(只读、不改文件)
paseo-committee两个不同 provider 的高推理 agent 并行做根因分析create_agent ×2 + 完成通知
paseo-plugin构建/管理可信的本地 Paseo 插件(原生面板、插件 RPC)插件脚手架与 paseo plugin CLI(接管了已删除的 paseo-loop 的位置)

技能里有三条约定值得单拎出来,因为它们是被真实教训逼出来的:

  • 对比是委员会的意义。 技能明确要求跨 provider 选人,而不是硬编码默认值。
  • 别催。 committee 技能写着「不要轮询、不要发催促」,理由是长时间思考本身是信号。
  • 偏好外置。 选哪个 provider 不写死在技能里,而是读 ~/.paseo/orchestration-preferences.json

CLI 侧则是同一批能力的人类入口(装配在 packages/cli/src/cli.ts:180-196):

子命令对应服务
paseo schedule create/ls/pause/resume/logs/run-onceScheduleService(cli/src/commands/schedule/)
paseo heartbeat create/update/delete同上,target 为已有 agent(cli/src/commands/heartbeat/)
paseo plugin …本地插件管理(cli/src/commands/plugin/,新增)
paseo agent ls/run/send/wait/…AgentManager(见 03)

(旧版还有 paseo loop …paseo chat … 两组子命令,已随 LoopService/聊天室一并移除。)


7. 边界与局限(诚实版)

原生路目前只有一家。 全仓只有 omp 声明 supportsNativePaseoTools: true。 其余 provider 全走 MCP,也就是说「零序列化、支持流式中间结果」这些好处,今天绝大多数用户享受不到。

MCP 路吃不到 sendUpdate mcp-server.ts:47 只把 signal 传下去,长耗时工具的中间进度在这条路上会丢。

能力令牌只防离机,不防同机。 令牌是本机随机值,同一台机器上任何能读到 agent 配置的进程都能拿到它。 守护进程没设密码时,/mcp/agents 直接放行(auth.ts:166-168)。

cron 是逐分钟扫描。 一年上界写死在 cron.ts:97,超出就抛错。秒级精度不支持。

递归深度没有全局闸。 代码里看不到「agent 开 agent 开 agent」的层数限制; 约束目前落在提示词与技能约定(如 committee 的「no edits」后缀)上,而不是守护进程强制。


8. 代码地图(导航索引)

主题文件路径符号名
工具目录的五个类型packages/server/src/server/agent/tools/types.tsPaseoToolCatalog PaseoToolDefinition PaseoToolExecutionContext PaseoToolRuntimeContext PaseoToolCatalogFactory
造目录 / 注册 / 分发packages/server/src/server/agent/tools/paseo-tools.tscreatePaseoToolCatalog registerTool executeTool parseToolInput
目录依赖清单packages/server/src/server/agent/tools/paseo-tools.tsPaseoToolHostDependencies
cwd / 工作区身份约束packages/server/src/server/agent/tools/paseo-tools.tsresolveScopedCwd resolveTerminalWorkspaceId resolveCallerAgent
结果与 schema 序列化packages/server/src/server/agent/tools/paseo-tool-serialization.tsaddModelVisibleStructuredContent formatStructuredContentForModel serializePaseoToolInputParameters
原生注入的开关判定packages/server/src/server/agent/agent-manager.tsbuildLaunchContext resolveProviderLaunchConfig setPaseoToolCatalogFactory
原生工具通道(OMP)packages/server/src/server/agent/providers/omp/host-tools.tsserializeOmpHostTools setOmpHostTools OmpHostToolRouter
provider 能力声明packages/server/src/server/agent/agent-sdk-types.tsAgentCapabilityFlags.supportsNativePaseoTools AgentLaunchContext.paseoTools
内置 MCP 的加与摘packages/server/src/server/agent/runtime-mcp-config.tswithRuntimePaseoMcpServer stripInternalPaseoMcpServer isInternalPaseoMcpServer
MCP 薄封装packages/server/src/server/agent/mcp-server.tscreateAgentMcpServer toMcpToolResult
MCP 路由与能力令牌packages/server/src/server/bootstrap.tsagentMcpAuthToken createAgentMcpSession runAgentMcpRequest
该路由的鉴权特例packages/server/src/server/auth.tsisAgentMcpRequestAuthorized shouldBypassBearerAuth
工具共用的等待与序列化packages/server/src/server/agent/mcp-shared.tswaitForAgentWithTimeout AGENT_WAIT_TIMEOUT_MS parseDurationString
完成通知闭环packages/server/src/server/agent/agent-prompt.tssetupFinishNotification sendPromptToAgent
定时引擎packages/server/src/server/schedule/service.tsScheduleService tick createOrReplace executeSchedule
cron 求下次触发packages/server/src/server/schedule/cron.tscomputeNextRunAt validateScheduleCadence
定时持久化packages/server/src/server/schedule/store.tsScheduleStore upsertByNameAndTarget
结构化回答与重试packages/server/src/server/agent/agent-response-loop.tsgetStructuredAgentResponse buildRetryPrompt extractJsonFromMarkdown StructuredAgentResponseError
两路语义的端到端测试packages/server/src/server/agent/mcp-parity.e2e.test.tscreateRecordingAgentClients
语音 speak 的挂接点packages/server/src/server/session/voice/voice-session.tsregisterVoiceBridgeForAgent
编排技能说明书skills/paseo-{handoff,advisor,committee,plugin}/SKILL.md
CLI 子命令装配packages/cli/src/cli.tscreateScheduleCommand createHeartbeatCommand createPluginCommand

一个容易走错的门:agent/provider-launch-config.ts 名字很像本章的东西,但它管的是 provider 可执行文件解析与环境变量(resolveProviderLaunchcreateProviderEnv), 和工具投递无关。原生注入的接线在 agent-manager.tsproviders/omp/ 里。