# pi-subagents

[English](README.md) | [中文](README.zh.md)

**为 pi 提供极简、完整的子代理框架。**

三个操作——启动、发消息、停止——配上独立上下文、并行执行、自动结果交付、可恢复会话和原生子代理窗口。生命周期完整，操作接口极简：你定义任务，框架管理代理。

作为 pi 扩展运行，除 pi 宿主提供的包外，没有额外运行时依赖。已在 pi 1.1.0 上测试。

```
你： 调研一下这个项目的数据库 schema
  → pi 调用 agent_spawn，spawn 一个常驻的 `pi --mode rpc` 子进程
  → 子 agent 在独立上下文窗口里独立工作
  → 结果返回；你继续聊天
```

## 为什么需要它？

Pi 没有内置 sub-agent。于是重活、并行的活、吃上下文的活，全都挤进你那一个窗口。pi-subagents 把活交给子 pi：噪音留在那边，只有答案回来。

## 特性

- **安静上下文** — 日志、搜索结果、测试输出留在子窗口里。你拿到最终结果，不是一片杂乱。
- **真正并行** — 一次起多个后台 agent，就绪结果在主代理下一个 turn 边界一起交付，每份保留独立卡片。无任务排队、无取结果工具。
- **全程可控** — `agent_send` 给运行中的 agent 发消息重定向（也能唤醒空闲的），`agent_stop` 停止进程。最终回复自动回传，承载阻塞问题和最终汇报；默认深度下子代理另有发往 `@parent` 的 `agent_send`，只用于非阻塞问题。
- **追问保留上下文** —— 子代理完成后默认常驻（空闲，零 token；每个约 180 MB 内存），进程已退出，或失败／停止后仍活着时，`agent_send` 都会从保存的会话自动恢复。子代理缺信息或需要决策时，把问题作为最后的回答结束；父代理用 `agent_send` 回答，子代理接着原来的上下文继续。非阻塞问题在轮中用 `agent_send` 发给 `@parent`，子代理接着做。
- **Pi 原生** — 渲染、会话、attach 全复用 pi 自己的机制；卡片看起来像内置工具——因为它们就是。
- **继承或覆盖** — 子 agent 默认继承你的模型与推理强度，也可逐个覆盖。便宜模型做侦察，强模型做实现。
- **无隐藏限制** — 默认无 token 上限、无超时、无并发上限。需要时用可选 `timeoutMs` 加一道护栏。
- **可复盘** — 每个会话持久化、永不删除；任何结果都能 `pi --session <path>` 回看。
- **按需嵌套** — 默认子代理不能再派子代理；在配置文件里调高 `maxDepth` 才允许。
- **Token 经济** — 系统侧保持克制：
  - **系统提示** — 工具描述只解释操作，协调规则放在按需读取的 skill 中。
  - **通知** — LLM 只看到最小结构化数据；装饰（label、用量）留在渲染层，永不进入 LLM 上下文。
  - **结果** — 自动截尾（2000 行 / 50KB）；展开任何卡片仍见全量。

## 设计理念

Pi 是一个极简、可扩展、由你定制的代理运行框架。pi-subagents 延续这个思路：提供完整的委派机制，不替你规定工作方式。

- **接口小，生命周期完整。** `agent_spawn`、`agent_send`、`agent_stop` 覆盖委派、追问和取消。结果自动送回，会话持久保存，代理可见、可查看。
- **提供机制，不做工作流引擎。** 不预设角色，不内置调度器，不强制编排。通过 prompt 定义任务，按需组合工具。协调建议放在可选 skill 中，而非运行时策略里。
- **复用 pi，而非另造一套基础设施。** 子代理是真正的 pi 进程。RPC、会话、渲染和模型配置沿用 pi 的能力，不引入另一套代理运行框架。
- **不用就不承担额外负担。** 代理工具按需启用，详细指导按需读取；UI 元数据不占模型上下文。嵌套和超时由你明确选择，不设隐藏默认限制。

极简是减少需要理解的概念，不是省掉代理生命周期。设计约定见 [SPEC.md](SPEC.md)，UI 层边界见 [docs/ui.md](docs/ui.md)。

## 安装

```bash
# npm（推荐）
pi install npm:@chthub/pi-subagents

# git
pi install git:github.com/chthub/pi-subagents

# 本地目录（开发）
pi install /path/to/pi-subagents
```

只在一次会话里试用：`pi -e /path/to/pi-subagents/index.ts`。

重启 pi 后直接说"让子 agent 去…"。

## 快速上手

### 发起一个任务

```
让一个子 agent 分析 src/ 下的认证逻辑
```

交互会话中，`agent_spawn` 默认后台运行：子代理在独立上下文里工作，最终回复自动送回。需要工具调用等待并内联返回答案时，指定 `run_in_background: false`。

### 后台并行跑多个

```
同时起三个子 agent 分别看 auth 模块、数据库层和 API 路由
```

Pi 在同一块里发三次 `agent_spawn`。子代理各自独立工作，后台就绪结果在下一个交付边界自动送回；前台调用同样并行执行，答案内联返回。无需轮询，也不需要额外的取结果工具。

### 发消息或停止

```
那个数据层子 agent——方案行不通，改用组合式重写
```

Pi 调用 `agent_send` 向运行中的 agent 注入 steering（当前模型响应及工具批次结束后、下一次模型调用前送达）。要停掉失控的 agent：“干掉那个后台子 agent” → `agent_stop`。两者对运行中的 agent 都适用；`persistent: true` 的话，完成后依然可寻址——`agent_send` 唤醒空闲的 agent 继续追问，`agent_stop` 结束。

## 启用方式

主会话里三个 agent 工具注册时**默认不激活**：不派子代理的对话里不带它们的参数说明。子代理里的 `agent_send` 一开始就是激活的。无参数的 `subagent_enable` 启用工具，下一次模型请求起生效。启用记录写进会话，resume 后仍然有效。

## 协调 skill

包内自带 [`pi-subagents-workflow`](skills/pi-subagents-workflow/SKILL.md)，是任务边界、并行、文件修改分工、后续工作和检查规则的唯一维护位置。不定义预设代理身份，也不在运行时强制工作流。

加载整个包后，新窗口默认就能看到 skill 的名称、触发描述和路径，不需要先调用 `subagent_enable`。模型读取 skill 或用户调用 `/skill:pi-subagents-workflow` 时，全文才进入上下文。包的 skill 过滤配置和 `--no-skills` 可以禁用发现。只用 `-e` 加载 `index.ts` 不会同时加载包内 skill。不能再派生子代理的子代理（默认深度下所有子代理都是如此）看不到这个 skill。

工具描述解释调用会发生什么，skill 说明主代理怎么组织工作。子代理即使没有读取 skill，也会收到一段解释最终回复自动回传的简短任务附言。

## 底部列表与子代理窗口

输入框下方列出本对话的所有子代理，一行一个（运行中 / 空闲 / 完成 / 失败 / 停止）：

```
 ⠋ @jude  只读目录检查   12s  bash: ls -la
 ‖ @yara  keeper        idle
 ✓ @max   分析 auth 模块  1m3s
 点击进入 · /agents
```

- **点击某一行**（pi 默认的全屏模式下可用），或输入 **`/agents`**（`/agents max` 直接打开），进入该子代理的全屏窗口：用 pi 原生组件显示它的完整对话，运行中实时更新。
- **在窗口里直接打字和它对话。** 这是侧聊：回答只留在子代理自己的会话里，**不会**通知主 agent。已结束的子代理在你发第一条消息时用 `pi --mode rpc --session <路径>` 复活，之后常驻（空闲）到主会话结束或被 `agent_stop`。失败／停止的旧进程仍活着时，这条新消息会先关闭旧进程，再恢复同一会话；额度耗尽不再导致切换账号后仍永久无法投递。
- **用 `/continue` 继续任务，并把结果回传父代理。** 在子代理窗口输入 `/continue`，或 `/continue 已切换账号，请重试`。这不是私聊：必要时恢复失败／停止的子代理，并将这一轮最终回复（或失败）自动通知父代理。运行中或空闲的直接子代理也能使用。普通消息仍是侧聊；`/continue` 由窗口本地处理，不代表支持完整的 Pi `/` 命令。
- **鼠标悬停**时高亮当前行。在 tmux、zellij、screen 里 pi 不开启鼠标移动上报，所以没有悬停高亮，但点击照常可用。
- **窗口底部是这个子代理自己的 pi 底边栏**：工作目录和 git 分支、它的 token 用量、费用和上下文占用、模型和推理强度，以及它的扩展设置的状态。窗口自己的按键提示放在最后一行。
- **连按两次 Ctrl+X** 停掉正在运行或常驻的子代理（第一次只是确认提示，中间按别的键就取消）。会话文件保留，之后发消息仍可复活。如果主 agent 还在等它这一轮的结果，会收到一条 stopped 结果，说明是用户停掉的。
- **Esc** 回到主对话；PgUp/PgDn 或滚轮滚动；全屏鼠标模式下可拖动右侧滚动条滑块、点击轨道跳转，拖到底部后重新跟随新输出。悬停或拖动时滑块从 `┃` 加宽为 `█`，不改变正文换行；悬停依赖终端上报鼠标移动。在正文上拖动可选中文字（双击选词、三击选行），松开后由 pi 复制，与主对话一致。ctrl+o 展开工具输出。
- resume 或 `/reload` 后列表会从会话记录重建（进程不会保留，会话文件会）。`/new`、`/resume`、`/fork` 也会停掉当前会话的子代理。

## 配置

### 配置文件

`<agentDir>/pi-subagents.json`（默认 `~/.pi/agent/pi-subagents.json`，跟随 `PI_CODING_AGENT_DIR`），也可以用 `PI_SUBAGENT_CONFIG` 指定路径。可选；启动和 `/reload` 时读取。

```json
{ "maxDepth": 1 }
```

| 键 | 默认 | 含义 |
|---|---|---|
| `maxDepth` | `1` | 主会话之下允许几层子代理。`1`：主会话能派子代理，子代理不能再派。`2`：子代理还能再派一层。以主会话的值为准，整棵树生效；到达上限的子代理只保留 `agent_send`，且只能发给 `@parent`。 |

值不合法时退回默认值，并在 TUI 里提示。

### 环境变量

| 变量 | 默认值 | 含义 |
|---|---|---|
| `PI_SUBAGENT_SESSION_DIR` | `<agentDir>/subagent-sessions/` | 子 agent 会话存放目录；设置可迁移位置。`<agentDir>` 为 pi 的 agent 目录（默认 `~/.pi/agent`，遵循 pi 的 `PI_CODING_AGENT_DIR`）。 |

目录遵循 pi 的 agent-dir 约定，刻意放在 pi 标准会话树之外，让 `pi -r`（resume）保持干净。会话文件永不删除。

### 工具参考

#### `agent_spawn` — spawn 一个隔离的 sub-agent

| 参数 | 类型 | 默认值 | 含义 |
|---|---|---|---|
| `prompt` | string | **必填** | 子 agent 的自包含任务描述。 |
| `label` | string | 必填 | 短标识，显示在工具卡、通知卡、widget 行和会话名上。 |
| `model` | string | 继承 | 覆盖子 agent 的模型。指定但注册表中找不到 → **报错，不静默降级**。 |
| `thinking` | `"off"`…`"max"` | 继承 | 覆盖推理强度；省略则用当前会话的级别。 |
| `tools` | string[] | 全部 | 子 agent 可见的工具白名单——白名单之外全部不可见。`agent_send` 总会保留，不用列出。 |
| `run_in_background` | boolean | `true` | 后台（默认）立即返回 `agent_id`，完成时投递携带最终输出的通知；`false` 阻塞到结果就绪。`pi -p` / json 模式下省略时走前台（宿主答完即退出，后台结果会丢）。子代理里没有这个参数，只能前台。 |
| `timeoutMs` | number | 无 | 可选的单轮时限（毫秒），作用于任务本身和之后每一轮由 `agent_send` 发起的对话。触发时扩展打断这一轮、等它 settled（等不到就强制停止），并按 stopped 报告。 |
| `persistent` | boolean | `true`（根会话）/ `false`（子代理内） | 完成后常驻（空闲，零 token），追问时不用重启；`agent_stop` 结束它。`false` 时完成即退出，`agent_send` 仍会从会话文件复活它。 |

最终回复自动回传，承载阻塞问题和最终汇报。默认 `maxDepth=1` 时子代理唯一的 agent 工具是发往 `@parent` 的 `agent_send`，只用于非阻塞问题，协调由父代理完成。

#### `agent_stop` — 取消代理任务

| 参数 | 含义 |
|---|---|
| `agent_id` | **必填** — `agent_spawn` 返回的 agent id。取消正在执行的任务并停止进程；明确要求停止空闲代理时只关闭进程。优雅终止（stdin EOF）；不生成新的完成通知。 |

停止不会删除保存的对话、撤销文件修改或取消已提交作业。空闲进程不消耗模型 token，随宿主会话退出而关闭。

#### `agent_send` — 树内发消息

| 参数 | 含义 |
|---|---|
| `to` | **必填** — agent_spawn 给到的 agent id（短人名，如 `max`），或 `"@parent"` 发给 spawn 你的会话。 |
| `message` | **必填** —— 消息文本；在目标当前轮结束后送达，唤醒空闲子代理，或从会话文件复活已结束的子代理（回答以通知送回）。 |

向空闲子代理发消息会启动新一轮模型调用；向已退出的子代理发消息会恢复保存的会话。最终回复自动返回。

工具只能寻址直接子代理，或者在子代理里寻址 `@parent`（它的直接父代理；到达深度上限的子代理只能发给 `@parent`）。父代理收到的是 `{kind: "non_blocking_question", agent_id, question}`，完成通知则带 `kind: "final_reply"`。发给 `@parent` 的消息不结束子代理这一轮：父代理在当前回复及工具批次结束后读到，回答在子代理当前这批工具调用结束后、或这一轮结束后出现在它的对话里。父代理若正阻塞在前台 `agent_spawn` 里，要等这次调用返回后才读到。不能直接投递给兄弟代理，跨层协调需要父代理转发。

## 进阶

### 指定模型

```
用 claude-sonnet 起一个子 agent 分析数据库设计
```

不指定模型 → 继承当前会话模型。`thinking` 同理：省略时继承当前推理强度，传 `"off"`…`"max"` 可覆盖。

模型**指定但注册表中找不到** → 报错，不静默降级。

### 限制工具

```
让子 agent 调研项目结构，但只允许用 read 和 grep
```

子 agent 看不到其他任何工具。只读探索 + 便宜模型是调研任务的推荐模式。

### 子进程启动集成事件

其他扩展可通过事件让子 RPC 进程加入自己的集成，不必依赖 pi-subagents，也不必另设 agent 目录。每次新建或复活 `AgentProcess` 前，pi-subagents 同步发出 `pi.events.emit("pi-subagents:child-launch:v1", launch)`，载荷是全新的可变对象：

```ts
{ env: Record<string, string>, extensions: string[], error?: string }
```

`env`、`extensions` 初始分别为 `{}`、`[]`。监听器必须同步完成，不能用 `async`、Promise 或延迟修改。例如，在扩展工厂中：

```ts
const unsubscribe = pi.events.on("pi-subagents:child-launch:v1", (data) => {
  const launch = data as {
    env: Record<string, string>;
    extensions: string[];
    error?: string;
  };
  try {
    launch.env.MY_CHILD_SYNC = "1";
    launch.extensions.push("/absolute/path/to/child-bridge.ts");
  } catch {
    launch.error = "Child integration unavailable";
  }
});
pi.on("session_shutdown", () => unsubscribe());
```

- 环境变量补充项覆盖继承的进程环境，但 pi-subagents 的子代理身份与深度字段优先。没有监听器时，环境继承行为不变。此机制本身不修改 `PI_CODING_AGENT_DIR`，也不传递凭据；请只注入非敏感协调信息，不要注入 token。
- 扩展路径去重后作为 `--extension <path>` 参数传入，新会话和带 `--session` 的复活均适用，不替代 Pi 原有扩展发现机制。建议使用绝对路径；相对路径以子进程工作目录为基准。
- 构造进程前会校验并复制载荷；pi-subagents 不把它写进 roster 或会话记录。环境变量、扩展字段的类型或字符串无效时拒绝启动，不做隐式类型转换。
- 只要提供了 `error` 就拒绝启动，非字符串等畸形值同样拒绝。请使用不含路径或 token 的通用错误文字；pi-subagents 只报告通用拒绝原因，不回显原文。事件发出本身的异常继续向上传递。
- **Pi 会在内部捕获并记录监听器异常。** 此钩子无法检测这些异常，也不等待监听器的异步工作。监听器必须自行捕获准备阶段的失败，并同步设置 `error`，才能确保失败时不启动子进程。
- 订阅清理由监听器所属扩展负责；pi-subagents 不注销其他扩展的处理器。

## 工作原理

每个 sub-agent 都是带持久化 session 的常驻 `pi --mode rpc` 子进程：

- **前台** — `agent_spawn` 等待子进程 settled，内联返回最终输出。非常驻子代理随后退出，常驻子代理保留以供追问。
- **后台结果** — `agent_spawn` 立即返回；子代理 settled 后，结果进入会话级收件箱。主代理忙碌时，所有就绪结果在当前回复及工具批次结束后、下一次模型调用前一起送达，也就是 steer 的交付时机；通过 `turn_end` 实现，`agent_before_settle` 兜底。不打断生成或工具，不进入 follow-up 队列，不依赖全局 steering 队列的逐条／全部消费设置。空闲时只合并一个事件循环 tick 内已就绪的结果，保留独立卡片、只启动一次模型处理，不等待尚未完成的子代理。这防止排队造成的延迟，不保证报告在后续文件修改后仍然代表最新状态。
- **发消息/停止** — `agent_send` 双向均用 steering（当前响应及工具批次结束后、下一次模型调用前），空闲子代理会被唤醒；`agent_stop` 关闭 stdin 优雅退出，不新增完成通知，但不删除已排队的有效结果。
- **常驻空闲** — 子代理默认常驻，完成后保持 idle（零 token）；之后 `agent_send` 可唤醒继续同一上下文，`agent_stop` 结束。完成通知与 widget 行带 `idle` 标记。
- **恢复／失败后继续** — 通过 `agent_send` 或子代理窗口发送一条**新消息**时，进程已退出，或仍活着但状态为 `failed`／`stopped` 的直接子代理，都会从保存的会话恢复。先等旧进程退出，再启动 `pi --mode rpc --session <路径>`，不重启健康的运行中／空闲进程。新消息开始下一轮，不自动重发之前失败的请求。旧进程清理后重新收集启动联动信息，因此已接入的 Codex 账号切换也作用于替代进程。并发恢复共用一次启动；明确停止或主会话关闭会取消待恢复操作。主代理追问仍回传结果，用户侧聊仍保持私有；已经尝试投递但失败的消息不会被盲目重试。
- **提问与交付** — 每个子任务末尾说明两条向上的通道怎么用。最终回复承载最终汇报和阻塞问题：被卡住时以问题结束本轮回复，等父代理用 `agent_send` 回答。发往 `@parent` 的 `agent_send` 只承载非阻塞问题：发出问题后接着做；不用它发阻塞问题、进度汇报或结果。完成后直接返回交付，结果自动回传，不额外发送一份完成报告。前台 spawn 的说明另加一句：父代理要等这次回复结束才读到轮中消息，所以非阻塞问题也写进回复。`agent_send` 默认对每个子代理开放，`tools` 白名单里不用列出。
- **Attach / 复盘** — sub-agent 会话存储在 `<agent 目录>/subagent-sessions/`（见[配置](#配置)）。在主会话里找到 session 路径（agent_spawn 调用结果或完成通知卡片），执行 `pi --session <path>`——通知里也带 path，直接问 LLM 也行。
- **Graceful turn limits（默认不限，opt-in）** — 不设隐藏 deadline：子 agent 一直跑到完成或被停，除非你传 `timeoutMs`。不做 token 限制——用量仅通知卡统计。

## 嵌套 sub-agent

默认关闭（`maxDepth: 1`）。调高 `maxDepth` 之后：

子 agent 是完整 pi 实例——全局安装本扩展后，它天然能再 spawn 子 agent。每一层都是独立进程、独立上下文；深度倍增启动时间与 token 成本。是否值得，由你（或模型）判断。

机制是纯点对点投递器：`agent_send` 按 id（短人名，如 `max`）投给直接子，或投给 `"@parent"`——其他目标显式报错。路由由 LLM 自行完成：每个 agent 只寻址自己被给到的 id，逐跳转发（孙联系祖 = 每层父 LLM 决定用 `@parent` 上转）。信息披露是唯一限制——父持有它 spawn 的 agent 的 id；没被告知的 id 谁也发不到。

## 成本与注意

- **Headless 下子 agent 随宿主退出**（`pi -p`）。主进程在响应结束时退出，后台子 agent 经 stdin EOF 被清理——从不留孤儿。后台流程（等通知、发消息、stop）面向常驻的 TUI 会话。
- **一子 agent 一进程。** 前台后台同是常驻 rpc 子进程。开多了 = 进程多了——请节制。
- **结果一次性投递。** 按进程及 settlement 的 `completion_id` 在当前运行会话内去重。同一代理不同轮次均保留，即使文本相同。主代理中断或报错后，结果只保存、不强行重启模型，供下一轮使用；若主会话先崩，结果仍在子代理 session 文件里（`pi --session <path>`）。
- **常驻代理占用内存。** 每个空闲代理仍是一个 pi 进程（典型扩展配置下约 180 MB RSS），直到主会话结束或被 `agent_stop`。一次性任务可设 `persistent: false`，之后仍能从会话恢复追问。
- **恢复依赖保存的会话。** 恢复的是对话上下文，不是被中断工具的内存或执行位置。

## 清理

pi 退出时，子 agent（运行中或 idle）都收到优雅的 stdin-EOF 关闭。session 留在磁盘供 attach/复盘；清理不会删除会话文件。

## 开发

开发、版本更新与发布规范统一见 [AGENTS.md](AGENTS.md)。

## 许可证

MIT。版权声明和第三方来源说明保留在 [LICENSE](LICENSE)。
