pi-proactive0.2.0-beta.2
给 Pi 增加主动性:空闲时主动推进或提问、按时间点主动发起任务、多个会话之间对话协作。(专有软件:分发物不含源码,使用条款见包内 LICENSE)
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-proactive
给 Pi 增加「主动」能力:空闲时主动推进或提问、按时间点主动发起任务、多个会话之间对话协作。
本包是 Pi 的一个扩展包(pi-package),加载后提供:
/proactive命令面:查看状态、开关主动性、排期任务、取消任务;schedule_task/message_agent/list_peers三个模型工具:模型可以自己排活、给另一个会话发消息、先查清有哪些对端在线/空闲/擅长什么再发(list_peers只读);- 跨进程协作层:同机 Unix socket 实时通道 + 文件邮箱兜底 + 消息去重。
许可与分发(专有)
本软件是专有软件(proprietary),非开源:源码不公开,分发物只含编译后的 JavaScript 产物。
- 许可:
package.json的license为SEE LICENSE IN LICENSE,协议正文 = 本包内的LICENSE文件(版权所有 / 仅限内部授权使用 / 禁止再分发 / 禁止反编译与再授权 / 授权期限与联系)。 - 分发物不含任何
.ts源码:安装包里只有build/(编译产物)、skills/、README.md、CHANGELOG.md、LICENSE。 - 内部分发走 tarball(
tools/release-pack.sh产出 + sha256);安装必须先解包再以目录方式装(直装.tgz会「显示成功」但宿主加载失败 —— 实测陷阱)。 - 发布前必须跑
bash scripts/ci/publish-preflight.sh --registry <url> --public(检查私有协议 / 零.ts源码 / 无内部路径与文档 / 入口在包内)。
安装
方式 A:私有 npm 源(组织 scope,跨机器可用)
pi install npm:pi-proactive
- 当前发布:
0.2.0-beta.1(dist-tagbeta/latest),integritysha512-q+fSY0OvPp8BeH1MGWSWqkhoVIjmZbQmd2ry5sPAQsqL8pvAQs0i1VrrAaLvy6Tds1l3fS01yBm/XzTl2bS9mQ== - 校验下载到的制品是否与发布一致:
npm view pi-proactive@0.2.0-beta.1 dist.integrity - 供应链缺口(如实披露):本次发布未带 npm provenance(本地发布无法生成;发布工作流模板见
scripts/ci/publish-npm.yml(复制到.github/workflows/后生效,需 GitHub token 的workflowscope),后续版本将从 CI 发布并自动带 provenance)。
方式 B:本地目录 / tarball(离线或内部试用)
pi install ./pi-proactive # 本仓库内
bash pi-proactive/tools/release-pack.sh --out ./dist # 产出**全量交付束**:tarball + sha256 + 交付目录 + zip + manifest,并跑四项自检(新鲜度/内容/校验记录值/zip 一致)
pi install ~/.pi/packages/pi-proactive # 先解包再按目录装(见下)
宿主会把安装源记成相对路径 → 请把包放在稳定目录里、不要移动它。
# 作为 pi 包安装(写入 ~/.pi/agent/settings.json)
pi install ./pi-proactive
# 本地试用:只加载扩展本体,不写入设置
pi --extension ./pi-proactive/extensions/proactive.ts
宿主自带的包(@earendil-works/pi-coding-agent、@earendil-works/pi-agent-core、typebox)只声明为
peerDependencies("*"),由宿主提供,不随本包安装(Pi 文档明确:把宿主包放进 dependencies
会产生物理副本、绕过扩展模块映射,导致重复类与重复注册)。
版本约束:本扩展的全部宿主 API 断言只对
@earendil-works/pi-coding-agent1.3.0 验证过。package.json的engines.pi写作>=1.3.0(最低可用版本),但更高版本未验证 —— 升级 pi 后可能出现行为变化,升级后请先重跑 runtime 探针再依赖本插件的自动行为。
5 分钟快速上手(内测版)
24×7 数字员工(常驻形态,默认关闭)
需要一个人不在场也在干活的常驻代理时再开(它会真花你的账号额度):
# 安装(幂等;macOS 用 launchd,Linux 用 systemd --user)
bash <安装目录>/tools/daemon-install.sh
# 看看它昨晚干了什么 + 有什么等你确认
/proactive doctor
- 默认
enabled=false+ 干跑 → 装了不花钱、不投递任何任务; - 开启后仍受预算硬上限约束(每小时/每日动作数;token 只做估算)→ 超限自动降级为只观察并记账;
- 不可逆动作绝不自动执行 → 进人工确认队列,早间交班时呈现;
- 形态:macOS
launchd(KeepAlive)/ Linuxsystemd --user(Restart=always); - 自愈:崩溃自动重启、幂等认领、半成品不重放;卸载用
tools/daemon-uninstall.sh(幂等、无残留)。
面向第一次拿到内测包的同事,按顺序做这 5 步就能看到插件真的主动一次。
步骤 1|装什么、装在哪
内测分发物是 tarball + sha256 校验文件(不是 git 仓库路径):
# 拿到 tarball 后先校验
cd <tarball 所在目录>
shasum -a 256 -c pi-proactive-<版本>.tgz.sha256
# 解包到一个**稳定目录**(内测包与仓库无关,但目录本身别乱挪 —— 见下面的说明)
mkdir -p ~/.pi/packages && tar -xzf pi-proactive-<版本>.tgz -C ~/.pi/packages
mv ~/.pi/packages/package ~/.pi/packages/pi-proactive
# 以 pi 包的形式安装(写入 ~/.pi/agent/settings.json)
pi install ~/.pi/packages/pi-proactive
为什么强调「稳定目录」:
pi install会把安装源记进settings.json的packages, 记的形式是相对 settings.json 所在目录的相对路径(宿主行为,见docs/packages.md)。 所以只要包目录本身不挪动,仓库怎么移动、改名都不影响; 但如果把包解到/tmp这类会被系统清理的位置,装完过一阵就会失效。 建议固定用~/.pi/packages/pi-proactive。
也可以直接从源码目录装(开发场景,注意源码目录挪动后要重新 pi install):
pi install ./pi-proactive # 相对路径:相对 settings.json 所在目录解析
pi install /abs/path/to/pi-proactive # 绝对路径:宿主会换算成相对 settings.json 的相对路径
自己打内测包:
bash pi-proactive/tools/release-pack.sh --out <产物目录>→ 产出 tarball +.sha256+ 交付目录(README / SKILL / ops / onboarding / LICENSE / CHANGELOG / DELIVERY / tools / tgz / sha256)+ zip +manifest.json(逐项 sha256 与大小);幂等:同输入同字节。 脚本自检四项,任一不符即非零退出:① 交付目录内的 tgz 与本次构建同哈希 ② 包内含src/peer-spawn.ts等当前能力文件 ③.sha256记录值与实算一致 ④ zip 与交付目录逐项一致。 复验既有交付束(不重新打包):bash pi-proactive/tools/release-pack.sh --check-only --out <产物目录>。
步骤 2|怎么确认装上了
pi list # 列表里应出现 pi-proactive
再开一个会话,执行下面这条命令,有回执就算装上了:
/proactive status
步骤 3|看一次状态(先看懂再动手)
/proactive status
输出是 7 行「人话 + 机器可读字段」,例如:
主动性:已开启 (enabled=true)
空闲触发:关 (idleTriggerEnabled=false)
空闲检测:关闭 (idleDetect=off)
空闲计时:—(未启用) (idleForMs=1823)
提问超时:2m (askTimeoutMs=120000)
限制:最短间隔 5m (minGapMs=300000) · 每小时上限 6 (maxPerHour=6) · 静默时段 未设置 (quietHours=无)
下次空闲触发:—(空闲触发未开启) (nextTriggerInMs=—)
最后一行会说明为什么触发 / 为什么没触发(排障先看这一行)。
步骤 4|让它第一次真的主动(两处都要改)
装上后插件默认不打扰(idleTrigger.enabled=false)。要看到第一次主动,需要两个开关:
# (a) 打开空闲触发(内存态:只对当次进程有效,重启后回到配置文件里的设置)
/proactive idle on
# ↳ 这一步同时会把 idleDetect 从 off 切到 input-only,否则开了也不会真的触发
# (b) 把阈值调小(默认 10 分钟太久,等不到)——改配置文件后重启会话
# <agent-dir> = $PI_CODING_AGENT_DIR,未设置时是 ~/.pi/agent
cat ~/.pi/agent/proactive/config.json
把 config.json 改成(只改这两处,其余字段保留):
{
"idleTrigger": { "enabled": true, "afterMs": 30000 },
"limits": { "minGapMs": 60000 }
}
afterMs: 30000= 空闲 30 秒就够触发(默认 600000 = 10 分钟);minGapMs: 60000= 两次主动之间至少隔 1 分钟(默认 300000 = 5 分钟)—— 不调它,即使阈值到了也会被冷却挡掉(判定说明会显示「冷却中」)。
改完重启会话,再 /proactive status,看 nextTriggerInMs 在往下走 → 到了就会主动推进一次。
想立刻自检链路,可以用 /proactive test(强制投递,绕过冷却与频率上限)。
步骤 5|怎么关掉
/proactive off # 关闭主动性总开关(已落盘,重启后仍生效;已排期任务保留)
/proactive idle off # 只关空闲触发(内存态)
/proactive on 可以再打开。彻底卸载见下面「卸载与回滚」。
典型场景脚本(可直接复制)
在 Pi 会话里直接粘贴命令;
#开头是注释,可以一起粘。
场景 1|空闲时主动汇报进度(最常用)
# 打开空闲触发并把阈值压到 30 秒(内存态部分 + 配置文件部分见「步骤 4」)
/proactive idle on
配置里设:
{
"idleTrigger": {
"enabled": true,
"afterMs": 30000,
"intent": "wake",
"prompt": "用户已空闲,请用一句话汇报当前进度,并给出下一步建议。"
},
"limits": { "minGapMs": 60000, "maxPerHour": 6 }
}
效果:用户停手 30 秒后,插件自己起一轮,把这段提示词投给模型,模型回一句话。
恢复安静:/proactive idle off。
场景 2|定时提醒(喝水 / 站起来 / 收工)
/proactive in 30m 起来活动一下,喝口水
/proactive at 18:30 该收工了,保存一下今天的进度
/proactive every 2h 看一下构建有没有挂
in <时长> <内容>:时长后触发一次(30s/10m/2h/1d/ 纯数字按秒);at <HH:MM> <内容>:到当天该时刻触发(已过则顺延到明天);every <时长> <内容>:按间隔重复。
查任务 / 取消:
/proactive tasks # 看 id、排期类型、相对到期、intent、归属、开关
/proactive cancel <id> # 取消(id 从上一条里抄)
⚠️ 定时任务与主动触发共用同一个冷却闸门:默认
minGapMs=300000(5 分钟), 到点投递可能顺延最多约 5 分钟。要准点,把limits.minGapMs调小。
场景 3|跨会话提问与回复(两个会话之间)
会话 A(发起方)直接跟模型说:
用 message_agent 工具给会话 "builder" 发一条 kind="ask" 的消息:
标题「进度同步」,正文「你那边跑到哪一步了?还剩什么没做完?」。
会话 B(对端)会收到提问,模型回答后自动回送一条 reply(携带原 correlationId)。
- 手工点名也可以:在会话 A 里执行
/proactive peers看在线 peer 的名字与 agentId; - 对端必须也装了本插件,否则消息不会被消费;
- 对端离线也能发:消息落盘到对端邮箱,等对方以同一 sessionId 重启后取回
—— 这种情况下回执是
queued(已落盘、尚未送达,不是「已送达」)。
场景 4|让模型自己排活(schedule_task 工具)
跟模型说:
帮我把「每 30 分钟检查一次 CI」排成一个重复任务,用 schedule_task 工具,when 传 30m。
模型会调用 schedule_task 真正落库(进任务表、进程退出后仍在),到期由调度器投递。
场景 5|只看不改(值班/观察模式)
/proactive status # 总览 + 为什么触发/为什么没触发
/proactive tasks # 待触发任务
/proactive peers # 在线 peer、心跳、当前任务
只读这三条不会产生任何投递动作。
卸载与回滚
停用(推荐先做这步)
/proactive off # 主动性总开关关闭(已落盘);已排期任务保留在任务表里
彻底卸载(三步)
# 1) 卸包
pi remove <安装时用的包路径或包名>
# 或直接编辑 ~/.pi/agent/settings.json 的 "packages" 数组,删掉 pi-proactive 那一项
# 2) 删运行期数据目录(可选,但这是「彻底」)
# 数据目录 = <agent-dir>/proactive/
# <agent-dir> = $PI_CODING_AGENT_DIR,未设置时是 ~/.pi/agent
rm -rf ~/.pi/agent/proactive # 先确认路径!别手抖删错目录
# 3) 确认
pi list # 列表里不应再有 pi-proactive
数据目录在哪、删了会丢什么
数据目录:<agent-dir>/proactive/(默认 ~/.pi/agent/proactive/),目录权限 0700、文件 0600。
删掉它 = 删掉本机全部运行期数据:
| 删掉的东西 | 后果 |
|---|---|
tasks.json |
所有排期任务消失(不可重建:排期只有这一个来源) |
mailbox/ |
未处理 / 待重试 / 已归档的消息全部消失(对端可能一直在等这些消息的回复) |
processed/(含 seen.json) |
去重账本消失 → 窗口内的重复消息会被重新处理(不丢消息,但会重复动作) |
threads/ |
线程日志(审计 + 轮次计数)消失 → 轮次上限判定失准(可能多跑几轮) |
agents/ |
在线名单消失 → 对端暂时找不到你(重新启动会话会重新注册) |
config.json |
配置回到默认值(enabled=true、idleTrigger.enabled=false) |
run/*.sock |
socket 端点(进程退出时会自动清理;僵尸端点下次启动会重建) |
只想静音、不想丢数据:只做
/proactive off,不要删数据目录。
回滚到旧版本
# 1) 备份当前数据(回滚前必做)
cp -a ~/.pi/agent/proactive ~/.pi/agent/proactive.bak.$(date +%s)
# 2) 装回旧版
pi install <旧版本包目录>
# 3) 重启会话,用 /proactive status 确认读到的是旧版配置
# 4) 若旧版读不懂新版数据(新版写了旧版不认识的落盘版本号)→ 恢复备份
# rm -rf ~/.pi/agent/proactive
# cp -a ~/.pi/agent/proactive.bak.<时间戳> ~/.pi/agent/proactive
当前落盘格式版本是 v1,没有 version 字段的文件按 v1 读(升级不丢数据),
所以在版本号不变的前提下,回滚不需要动数据。
反馈渠道(内测)
报给谁:直接报给发给你这个 tarball 的同学(内测发包人);也可以在本仓库开 issue 记录。
请一并带上这三样(缺一样都很难复现):
- 版本:
pi-proactive的版本号(tarball 文件名里就有,如0.2.0-beta.1); /proactive status的完整输出(它含enabled/idleDetect/minGapMs/maxPerHour/quietHours/nextTriggerInMs与「为什么触发/为什么没触发」的判定说明);- 宿主版本:
pi --version(本扩展的宿主 API 断言只对 pi 1.3.0 做过验证)。
再附上(有则更好):复现步骤、期望 vs 实际、插件 stderr 里带 [proactive] 前缀的日志片段、
<agent-dir>/proactive/ 下相关文件(如 tasks.json、某个信封)。
内测边界(先知道这些,免得误判)
这是内测版,以下四条是当前已知边界,不是待修的 bug 清单:
- 跨机行为未验证 —— 跨主机(共享目录 / NFS)只有落盘排队(约一个轮询周期,秒级), 掉线或过期都可能导致消息不被处理。内测期请限定同机使用。
- 无鉴权 —— 威胁模型是「同 UID 可信」:同 UID 的任意进程可以往邮箱写文件并冒用
from。 本插件不是安全边界,不要用它防御同机其它进程。 - TUI 完整链路未验证 —— 真人按键下的「提问 → 回答 → 回填」完整闭环只做了部分场景抽验
(见
docs/review/tui-smoke.md自述的 7 项未验证项);rpc / json / print 等非交互模式的降级行为有实测。 - 定时投递可能被 5 分钟冷却顺延 —— 排期任务与主动触发共用同一个冷却闸门
(默认
limits.minGapMs=300000),到点投递可能晚约 5 分钟。要准点就调小minGapMs。
其它未验证项(长时驻留 > 10 分钟、every 的小时级节拍、跨模型家族独立复验等)
见下方「能力边界与限制」一节,那里是逐条如实列出的完整清单。
命令(Usage)
Usage: /proactive <子命令>。下表逐条对应实现里实际注册的分支(src/commands.ts 的命令处理
switch),未列出的子命令会被拒绝并提示可用清单。
命令面真源 = 实现:下表由
scripts/gen-doc-tables.sh从pi-proactive/src/commands.ts的SUBCOMMANDS+switch (sub)的 case 分派自动生成(共 16 个命令),不要手改; 要改命令面请改实现,然后跑bash scripts/gen-doc-tables.sh --apply(门禁scripts/check-doc-tables.sh校验)。
| 命令 | 参数形态 | 做什么 | 状态 |
|---|---|---|---|
/proactive status |
无 |
总览:开关、空闲计时、待触发任务、对端在线、上次主动、本小时用量。 | 已实现 · pi-proactive/src/commands.ts:550 |
/proactive on |
无 |
打开全部「代理自发」行为(空闲触发、模型自调度、跨会话自发请求)。 | 已实现 · pi-proactive/src/commands.ts:565 |
/proactive off |
无 |
关闭全部「代理自发」行为;已排期任务保留但不再触发。 | 已实现 · pi-proactive/src/commands.ts:566 |
/proactive test |
无 |
立即模拟一次触发(走完整「护栏 → 投递」链路),用于验证主动能力是否真的生效。 | 已实现 · pi-proactive/src/commands.ts:644 |
/proactive tasks |
无 |
列出所有待触发任务(编号 / 类型 / 下次到期 / 归属 / 触发次数)。 | 已实现 · pi-proactive/src/commands.ts:647 |
/proactive cancel |
<id> |
取消一个任务。 | 已实现 · pi-proactive/src/commands.ts:673 |
/proactive idle |
on|off |
打开 / 关闭空闲触发并设定阈值(on 可带时长参数,省略 = 用上次设定)。 | 已实现 · pi-proactive/src/commands.ts:713 |
/proactive peers |
[--all|--idle|--name <子串>|--detail <agentId>] |
列出已注册的对端与在线状态(名字 / 忙闲 / 最后心跳);可加 --all / --idle / --name / --detail。 | 已实现 · pi-proactive/src/commands.ts:586 |
/proactive spawn |
[名字] |
创建一个对端会话(子进程 pi --mode rpc);本会话退出时一并终止。 | 已实现 · pi-proactive/src/commands.ts:621 |
/proactive in |
<时长> <内容> |
排一个延迟任务(默认只属于当前会话)。 | 已实现 · pi-proactive/src/commands.ts:691 |
/proactive at |
<HH:MM> <内容> |
排一个绝对时间任务。 | 已实现 · pi-proactive/src/commands.ts:692 |
/proactive every |
<时长> <内容> |
排一个周期任务。 | 已实现 · pi-proactive/src/commands.ts:693 |
/proactive help |
无 |
打印命令速查表(命令表 / 配置键 / 最常见故障 / 详细文档路径)。 | 已实现 · pi-proactive/src/commands.ts:728 |
/proactive history |
[peer|me] [条数] |
查看最近的主动行为记录(时间 / 类型 / 目标 / 结果 / 被护栏拦下的原因)。 | 已实现 · pi-proactive/src/commands.ts:651 |
/proactive doctor |
[--json] |
只读体检:逐项健康判定 + 处置建议(--json 机器可读);不打扰、不耗额度。 | 已实现 · pi-proactive/src/commands.ts:558 |
/proactive tell |
<peer> <内容> [--kind ask|task|notify] |
人侧给另一个会话下指令(--kind ask|task|notify,缺省 ask 要回话);回信落回本会话。 | 已实现 · pi-proactive/src/commands.ts:641 |
排期命令的排期结果会当场回执(任务 id、相对到期、绝对到期),任务落盘在任务表里,进程退出后仍在, 下次启动继续按表调度。
注:
/proactive quiet不是命令(输入会被拒绝;静默时段是配置项limits.quietHours);/proactive notify不是命令(notify是消息语义kind:"notify",由模型工具message_agent承载, 人侧给对端下指令用/proactive tell);/proactive history [peer|me] [条数]已实现。 各命令的行为细节(落盘 / 内存态、参数怎么写、失败怎么提示)见下方「典型场景脚本」「配置」「排障」三节。
模型工具
| 工具 | 参数 | 作用 | 定义位置 |
|---|---|---|---|
schedule_task |
prompt(必填):任务提示词 intent(可选):投递意图,默认 wake(取值:wake | interrupt | queue | notify) askUser(可选):触发时是否向用户提问(仅本地) when(可选):触发时间:时长(10m)/ 时刻(18:30)/ ISO 8601;缺省按 10m 处理 |
预约一个稍后触发的任务(真正落库到任务表,到点由调度器投递)。when 支持时长(10m)、时刻(18:30)或 ISO 时间;缺省按 10m 处理。 | pi-proactive/extensions/proactive.ts:624 |
message_agent |
to(必填):目标 agent 的名字或 agentId kind(可选):消息语义(对方要不要回我);缺省 notify(取值:notify | ask | task | reply | broadcast) subject(可选):标题 body(必填):正文 intent(可选):投递意图(怎么投给对方);缺省 wake(取值:wake | interrupt | queue | notify) threadId(可选):话题 / 会话 id(可选) |
给另一个正在运行的 agent 会话发消息(跨进程协作)。对端在线时走 socket 直达;不在线时消息落盘到对端邮箱,待其上线后处理。 | pi-proactive/extensions/proactive.ts:669 |
list_peers |
name(可选):名字子串(大小写敏感;也命中 agentId)。同名歧义时返回与 message_agent 报错完全一致的候选 status(可选):按在场状态过滤(online = 在线 = 空闲 ∪ 忙碌)(取值:online | idle | busy | offline) capability(可选):按能力标签过滤(如 message_agent) includeOffline(可选):是否包含离线对端(缺省 false = 只给在线) |
列出本机可见的其它 agent 对端(名字 / agentId / 在场状态 / 心跳龄 / 能力标签 / 在途任务数 / 传输方式),可按名字、状态、能力过滤。只读:不产生任何对外行为。发消息前先查它(先查后发),别盲发 —— 离线对端现在打扰它不合适。 | pi-proactive/extensions/proactive.ts:743 |
proactive_query |
action(必填):查询动作(与 /proactive 子命令同名)(取值:peers | history | status | tasks | doctor | help) name(可选):peers:名字子串过滤 all(可选):peers:是否包含离线对端 idle(可选):peers:只看空闲对端 detail(可选):peers:只看这一个 agentId scope(可选):history:范围(对端 / 我)(取值:peer | me) peer(可选):history:按对端 agentId 过滤 limit(可选):history:条数(1-200 的整数) json(可选):doctor:输出 JSON |
查询域:查对端(peers)/ 看主动行为历史(history)/ 看状态(status)/ 看任务(tasks)/ 只读体检(doctor)/ 看命令手册(help)。只读:不产生任何对外行为。 | pi-proactive/extensions/proactive.ts:780 |
proactive_schedule |
action(必填):排程动作(与 /proactive 子命令同名)(取值:in | at | every | cancel) when(可选):in/at/every:触发时间(时长 10m / 时刻 18:30) content(可选):in/at/every:任务内容 id(可选):cancel:任务 id |
排程域:定时(in)/ 定点(at)/ 周期(every)/ 取消(cancel)。落到真实任务表,到点由调度器投递;与命令面 /proactive in|at|every|cancel 同源。 |
pi-proactive/extensions/proactive.ts:806 |
proactive_peer |
action(必填):对端动作(与 /proactive 子命令同名)(取值:tell | spawn) peer(可选):tell:目标对端(名字或 agentId) content(可选):tell:指令内容 kind(可选):tell:指令类型(缺省 ask)(取值:ask | task | notify) subject(可选):tell:标题 thread(可选):tell:线程 id name(可选):spawn:对端名字 |
对端协作域:给对端下指令(tell)/ 创建对端会话(spawn,受同时存活数上限)。tell 的目标解析与回执三态与命令面 /proactive tell 完全同源。 |
pi-proactive/extensions/proactive.ts:825 |
proactive_manage |
action(必填):管理动作(与 /proactive 子命令同名)(取值:on | off | idle | test) state(可选):idle:开 / 关(取值:on | off) |
管理域:主动性总开关(on/off)、空闲触发开关(idle)、自检(test)。这些能力属 human-gated:调用只会回报「需人工确认」并给出可复制的人工指令,不会自行改动全局开关。 | pi-proactive/extensions/proactive.ts:847 |
模型工具面真源 = 实现:上表由
scripts/gen-doc-tables.sh从pi-proactive/extensions/proactive.ts的pi.registerTool({ name: … })注册项与参数 schema 自动生成(共 7 个模型工具),不要手改; 表中参数即真实 schema。下面三个小节是逐个工具的展开说明。
schedule_task
预约一个稍后触发的任务(真正落库到任务表,到点由调度器投递)。
| 参数 | 类型 | 说明 |
|---|---|---|
prompt |
string(必填) |
任务提示词 |
intent |
"wake" | "interrupt" | "queue" | "notify" |
投递意图,缺省取配置里的 idleTrigger.intent(默认 wake) |
askUser |
boolean |
触发时是否向用户提问(仅本地)。提问必须带超时;用户取消与超时是两个结局:取消 → 不消耗额度、不记待答(只留一条 declined 审计事件)、文案「已取消」;超时 → 消耗额度、记待答(pending)、文案「未在时限内收到回答,已记为待答」。额度在提问有结论之后才记账(回答 / 超时才扣,取消不扣);提问窗口内不叠加提问与本机投递 |
when |
string |
触发时间:时长(10m)/ 时刻(18:30)/ ISO 8601;缺省按 10m 处理 |
返回:details.created === true 时带 taskId / dueAt / schedule;参数非法或任务表未初始化时
details.created === false 并带 error 文案。
list_peers
只读查询对端目录:谁在线、谁空闲、谁擅长什么、有多少在途请求。
- 参数:
name(名字子串)|status(online/idle/busy/offline)|capability(能力标签)|includeOffline(默认只给在线) - 返回:名字 / agentId / 在场状态 / 心跳多久前 / 能力标签 / 在途任务数 / 传输方式
- 为什么要先查:对端离线时打扰它不合适;多个对端同名时
message_agent会因歧义直接报错 →list_peers会把候选列给你
message_agent
给另一个正在运行的 agent 会话发消息(跨进程协作)。
| 参数 | 类型 | 说明 |
|---|---|---|
to |
string(必填) |
目标 agent 的名字或 agentId |
kind |
"notify" | "ask" | "task" | "reply" | "broadcast" |
消息语义(对方要不要回我),缺省 notify |
subject |
string |
标题 |
body |
string(必填) |
正文 |
intent |
"wake" | "interrupt" | "queue" | "notify" |
投递意图(怎么投给对方),缺省 wake |
threadId |
string |
话题 / 会话 id(可选) |
返回 details.status,共 8 种,语义严格区分:
| status | 含义 |
|---|---|
accepted |
已送达:对端在线并已接收 |
duplicate |
对端已处理过这条消息,未重复投递 |
queued |
已落盘、尚未送达 —— 有两种语义,都不得写成「已送达」:① 对端离线待上线(消息写入对端邮箱,对方上线后才收到);② 对端在线但本轮未接收(原因随附,如对端处理抛错 / 投递失败) |
rejected |
被拒绝(信封非法 / 参数非法 / 找不到目标 agent) |
expired |
消息已过期,对方不会再处理 |
error |
投递失败(写盘失败等) |
unreachable |
对端不可达(端点不存在或无进程监听) |
timeout |
等待回执超时(对端未在时限内回执) |
queued不是「已送达」。 它有两种语义:对端离线待上线(消息落在对端邮箱,对方以同一 sessionId 重启后取回)与对端在线但本轮未投递(原因随附)。两种情况下对端都尚未收到。
kind:"ask" 时,对端模型回答后会自动回送一条 kind:"reply"(携带原 correlationId、继承 threadId)。
同一线程的往返上限 = 4 轮(只计已完成的 reply;未获回答的提问不占额度;投递失败的回信不虚增计数)—— 达到上限后不再回信,并必须通知请求方(该通知走 socket 优先 + 邮箱兜底:离线请求方也能收到——落盘到其邮箱、待其上线后取回;去重粒度=(线程, 请求方),同一线程同一请求方只通知一次;见下方残余限制①)。
配置
配置文件:<agent-dir>/proactive/config.json
(<agent-dir> = $PI_CODING_AGENT_DIR,未设置时为 ~/.pi/agent)
{
"enabled": true,
"idleDetect": "off",
"askTimeoutMs": 120000,
"idleTrigger": {
"enabled": false,
"afterMs": 600000,
"intent": "wake",
"askUser": false,
"prompt": ""
},
"limits": {
"minGapMs": 300000,
"maxPerHour": 6,
"quietHours": { "from": "22:00", "to": "08:00" }
},
"peerWatch": {
"enabled": false,
"afterIdleMs": 600000
}
}
| 配置键 | 默认值 | 作用 | 定义位置 |
|---|---|---|---|
enabled |
true |
主动性总开关(关掉则定时投递与空闲触发都不发生)。 | pi-proactive/src/config.ts:336 |
idleDetect |
"off" |
空闲检测档位:terminal(仅 TUI 生效)/ input-only(只靠输入事件刷新基线)/ off(关闭)。 | pi-proactive/src/config.ts:337 |
askTimeoutMs |
120000 |
提问等待用户回答的超时(毫秒)。 | pi-proactive/src/config.ts:338 |
idleTrigger.enabled |
false |
空闲触发开关。默认关闭 → 装上插件不会立刻开始打扰。 | pi-proactive/src/config.ts:340 |
idleTrigger.afterMs |
600000 |
空闲多久后触发(毫秒)。 | pi-proactive/src/config.ts:341 |
idleTrigger.intent |
"wake" |
空闲触发的投递意图。 | pi-proactive/src/config.ts:342 |
idleTrigger.askUser |
未设 |
空闲触发时是否向用户提问。 | pi-proactive/src/config.ts:432 |
idleTrigger.prompt |
"" |
空闲触发的提示词。 | pi-proactive/src/config.ts:343 |
limits.minGapMs |
300000 |
两次主动触发之间的最短间隔(毫秒,护栏冷却)。 | pi-proactive/src/config.ts:346 |
limits.maxPerHour |
6 |
每小时主动触发次数上限。 | pi-proactive/src/config.ts:347 |
limits.quietHours |
未设 |
静默时段 {from,to}(HH:MM,from 大于 to 表示跨零点),时段内不主动打扰。 | pi-proactive/src/config.ts:386 |
peerWatch.enabled |
false |
跨会话空闲协作(peer watch)开关。 | pi-proactive/src/config.ts:351 |
peerWatch.afterIdleMs |
600000 |
连续空闲多久后才去询问 peer(毫秒)。 | pi-proactive/src/config.ts:352 |
version |
"v1" |
落盘格式版本;无法识别时拒绝按已知格式解析(保留原文件,不覆盖)。 | pi-proactive/src/config.ts:516 |
配置面真源 = 实现:上表由
scripts/gen-doc-tables.sh从pi-proactive/src/config.ts的defaultConfig()/mergeConfig()/loadConfig()自动生成,不要手改; 键集合、默认值与定义位置都以生成表为准。
配置文件缺失或损坏时回退到默认值,不会抛错。
运行时产生的状态目录(都在 <agent-dir>/proactive/ 下):
| 路径 | 内容 |
|---|---|
tasks.json |
任务表(排期任务;每条记录带 version:"v1",版本不识别的文件会被忽略并记日志) |
agents/<agentId>.json |
在线 agent 注册表(含心跳时间戳) |
run/<agentId>.sock |
Unix domain socket 端点(同机实时通道) |
mailbox/<agentId>/ |
文件邮箱(socket 不可用时的落盘兜底) |
processed/<agentId>/ |
已处理信封与去重集合 seen.json 的归档位置 |
mailbox/<agentId>/claimed/、expired/、corrupt/ |
认领中 / 过期 / 损坏信封的归档位置 |
配置外置(提示词 / agent 定义)
配置里篇幅会长、要人工编辑的内容(提示词、agent 角色说明、rubric、模板)改用独立文件维护, 配置文件只留标量与引用。布尔开关 / 数字阈值 / 短枚举不要外置——那只会把「看一眼配置」变成「跳目录猜」。
目录约定(相对 config.json 所在目录,即 <agent-dir>/proactive/):
prompts/<ref>.md # 提示词
agents/<ref>.md # agent 定义
引用写法:
{
"idleTrigger": { "promptRef": "idle-probe" },
"contentRefs": { "reviewer": { "agentRef": "example-reviewer" } }
}
promptRef→prompts/<ref>.md;agentRef→agents/<ref>.md;- 文件可选带 front-matter(
---包裹的key: value块),其余部分是正文; - 优先级:同层仍有内联
prompt(旧配置)时内联优先,并给一次性提示(ref 不生效); - 失败即报错:文件不存在 / 空文件 / 格式错 / 路径越界(
../)→ 加载失败并指名 ref 与路径, 不会静默回退默认值; - 解析后的路径必须留在配置根目录内(越界路径与指向外部的符号链接都拒绝);
- 内容单次读入快照(外部程序同时改文件也不会读到半截内容)。
示例:pi-proactive/agents/example-reviewer.md —— 复制到 <agent-dir>/proactive/agents/ 后
即可被上面的 agentRef 引用。
<agent-dir>/proactive/agents/下同时存在两种东西:对端注册表agents/<agentId>.json与外置 agent 定义agents/<ref>.md—— 按扩展名区分,互不覆盖。
空闲判定基线(按实测修订)
空闲判定现在用**「最近任何活动」**做基线:用户输入 + 代理活动(模型回合起止 / 工具调用 / 助手消息落定)都会把计时归零。 代理在干活时不再被当成「用户离开了」。
- 两条基线:
lastUserActivityAt(只有用户输入刷新;idleDetect三档的差异只决定用户输入是否计入)+ 「最近任何活动」(判定用)。 - 触发前必须确认代理已交回控制权:回合进行中 / 工具链未结束时判定原因为「代理正在忙」,不触发。
- 插件自己投递的主动消息不刷新任何基线 —— 否则插件会被自己的动作无限推迟下一次触发(自锁)。
- 阈值维持 10 分钟(
afterMs=600000)+ 5 分钟冷却(minGapMs=300000)。
实测数字(日志级测量;同一份会话日志数据集、同一 10 分钟阈值;来源:pi-proactive/docs/idle-ground-truth.md):
| 配置 | 工作期误触发率 | 漏报率 | 离开覆盖率 | 触发时机正确率 |
|---|---|---|---|---|
| 旧基线(只算用户输入)+ 冷却 | 20.07% | 55.07% | 44.93% | 20.80% |
| 新基线(任何事件都刷新)+ 冷却 + 忙碌门控 | 3.01% | 68.77% | 31.23% | 73.31% |
表述纪律:在日志级测量下,工作期误触发率由 20.07% 降到 3.01%;这不等于「误触发率已消除」。
这份测量不能证明什么(如实披露):
- 不能证明「用户离开了」—— 只能证明「用户没有交互」;用户可能在读屏幕(与离开在日志里同形)。
- 不能证明某个阈值对某个用户 / 场景最优 —— 结论只在「单用户 · 重度 agentic 编码」场景内成立。
- 不能证明「打扰率 = 20.07%」等于用户被打扰的真实概率 —— 护栏、频率上限与投递成功率未建模。
- 不能证明新基线「一定更好」—— 漏报率从 55.07% 升到 68.77%(更少被叫醒,换更少打错时机)。
- 不能评估
terminal档 —— 终端按键在会话日志里不可见。 - 不能替代真人实验 —— 这是日志级(实现级 / 测试级)证据,不是现场级证据。
能力边界与限制(如实披露)
- 对端必须加载本插件,跨会话 / 跨进程协作才成立。对方没装插件 → 消息永远不会被消费。
- 跨机器只有落盘排队(~1s),没有实时通道。 同机走 Unix socket 直连;跨机(共享目录 / NFS) 只能靠文件邮箱轮询,延迟约一个轮询周期(秒级),且不可靠(掉线、过期都可能导致消息不被处理)。
- 对端正在流式输出时收到 reply,只会多出一个 turn(
turn_start),不会多出一个新的agent_start。 reply 以deliverAs:"followUp"注入同一个 agent run;对端空闲时收到 reply 才走triggerTurn:true起新 run, 那时才出现新的agent_start。按「agent_start计数」判定「被唤起了几轮」的监控 / 验收脚本会因此误判。 - 跨会话 / 跨进程协作要求双方都加载本插件。
- agentId =
<name>#<host>#<sessionId>(name= 工作目录 basename,host= 主机名,sessionId= 宿主会话 id):会话稳定(同一会话跨进程重启地址不变)、会话间唯一 (不同会话不会互相投递)。宿主未提供会话 id 时退回<name>#<host>#<pid>(进程级唯一, 能力退化但不报错);PI_PROACTIVE_AGENT_ID可显式覆盖。 - 离线投递:对端优雅退出 → 注册表保留条目并标
offline→ 发送方仍可解析 → 消息落盘到对方邮箱 → 对方以同一 sessionId 重启后取回。只有从未注册过的纯名字 才返回error(无法确定邮箱目录,不得谎称已排队)。 - 空闲触发默认关闭(
idleTrigger.enabled=false),不显式打开前不会打扰用户。 - 残余限制(如实列出,不掩盖):
① 达线程轮次上限的通知走 socket 优先 + 邮箱兜底 —— 离线请求方也能收到(落盘到其邮箱、待其上线后取回),但不是实时;去重粒度=(线程, 请求方)(同一线程同一请求方只通知一次,不同请求方各自都被交代);
② 注入标记串会出现在模型可见正文与转录里(用于精确识别「本插件自己注入的消息」,代价是正文多一串标记);
③ 旧的
scope:"session"任务(无ownerSessionId)在进程重启后不再匹配 —— 归属退回按ownerAgentId判定,而旧任务里的ownerAgentId是旧的 pid 形态、与当前的会话形态 agentId 不一致 → 这些旧任务不再被投递; ④ socket 路径过长会退化(macOS 上限约 104 字符)→ 退化为文件邮箱落盘排队(延迟变大,功能不中断); ⑤ 注册表条目永不删除 —— 优雅退出只标offline不删文件 →agents/目录会缓慢积压; ⑥ TUI 完整链路(提问 → 回答 → 回填)未验证 —— pty 档只做了部分场景抽验,完整链路未跑通(见docs/review/tui-smoke.md的 7 项未验证); ⑦ 跨家族独立复验未做(本机只有 minimax 一个模型家族); ⑧ 跨机行为未验证(仅落盘排队 ~1s); ⑨ >10 分钟长时驻留未验证 —— 实测中每个宿主生命周期 ≤ 4 分钟,长驻下的资源 / 孤儿进程 / 内存行为未观测; ⑩every的小时级节拍未验证 —— 短时实测节拍正常,小时级是否漂移未观测; ⑪ 默认/proactive idle on只切到input-only档 —— TUI 下不订阅终端输入(attachTerminalInput只在terminal档调用)→ 敲键本身不刷新空闲基线(基线只在宿主发出input事件时刷新);需要按终端输入判定空闲,请把配置idleDetect设为terminal; ⑫ 定时投递可能因与主动触发共享冷却而被顺延(最多约 5 分钟) —— 默认minGapMs=300000(5 分钟),排期任务与主动触发共用同一个冷却闸门(Guard.check()返回defer→ 任务的dueAt顺延);需要准点投递请调小minGapMs。 - 威胁模型 = 同 UID 可信。 同 UID 进程之间没有认证,信封里的
from是自声明字段 —— 本插件不是安全边界,不要用它防御同机其它进程。 - 护栏按 agent 作用域计数。 每个 agent 进程各有一份护栏状态 ⇒ N 个 agent ⇒ N × 单 agent 上限, 不是全局上界。
- 适用版本 pi 1.3.0。 全部宿主 API 断言只对
@earendil-works/pi-coding-agent1.3.0 验证过; 更高版本未验证。 - TUI 档 pty 仅抽验部分场景(
docs/review/tui-smoke.md:该报告自述不构成验收结论、并列 7 项未验证,其基线早于提问路径重写 → 需重跑才算数);rpc / json / print 等非交互模式的降级行为已实测。 - 跨进程端到端实测(V-03)已完成(
docs/review/cross-process-e2e.md:两个真实 Pi 宿主 + 真实模型回合)。 ⚠️ 该基线早于「agentId 会话稳定化」与 M5c 修复轮 → 合并态已复测部分场景(核心闭环与达上限通知,见docs/review/final-acceptance.md§7.5/§8),原 11 场景在合并态的逐条复测未做。 - 协作接线层(M5c)独立验收首轮判「不通过」,修复轮已合入,第三轮独立复验已出结论:有条件通过(S-1
S-8 全关、P-1P-7 全关;仍有 3 项 Minor 残余与若干未验证项,见docs/review/m5c-reverify-3.md) → 该层不得称已验收通过。 /proactive on|off已落盘(原子写<agent-dir>/proactive/config.json的enabled字段),重启后仍生效;/proactive idle on|off不落盘,仅当次进程内有效(重启后回到配置文件里的设置)。
排障
先看插件日志(stderr,日志前缀 [proactive]),再看状态:/proactive status 会直接给出
「为什么触发 / 为什么没触发」的判定说明。
| 现象 | 排查方向 |
|---|---|
| 任务没触发 | ① /proactive status 看 enabled 是否为「已开启」——off 时定时投递与空闲触发都不发生;② 看「下次空闲触发 / 限制」行确认是否被冷却(minGapMs)、每小时上限(maxPerHour)、静默时段挡下;③ 确认任务在 /proactive tasks 里且 enabled=true;④ 任务归属是 scope=session,由会话 id 判定(会话 id 跨进程重启稳定);若宿主未提供 session id,则退回按进程归属兜底 —— 这种情况下进程重启后旧任务不再归新进程所有 |
消息一直 queued |
queued = 已落盘、尚未送达,两种语义:① 对端离线待上线 —— 查 <agent-dir>/proactive/mailbox/<对方 agentId>/ 是否有文件;对方以同一 sessionId 重启后会自动取回(agentId 会话稳定 → 邮箱路径不变);若对端从未注册过(纯名字)则直接 error。② 对端在线但本轮未接收 —— 回执附原因(对端处理抛错 / 投递失败),消息同样落在对方邮箱待重试。两种都不是「已送达」 |
| 空闲不触发 | ① 空闲触发默认是关的 → /proactive idle on(不落盘,仅当次进程内有效)或改配置 idleTrigger.enabled=true;② idleDetect 默认是 off,没有输入信号时不会猜你在不在 → 设成 terminal(仅 TUI)或 input-only;③ 非 TUI 模式下 terminal 会自动降级为 input-only;④ 确认不是「代理正在忙」(busy)或没到阈值(threshold) |
| 对端找不到 | message_agent 的 to 按 agentId 精确匹配 → 名字唯一匹配 → 前缀唯一匹配 解析(同名 / 前缀命中多个会直接报错);离线条目同样可解析(优雅退出保留条目并标 offline,心跳过期=崩溃也可解析)→ 消息落盘待其上线;只有从未注册过的纯名字才 error。注册表条目靠 30s 心跳刷新,超过 3 个心跳周期未刷新即视为离线 |
| socket 起不来 | 端点被占用(另一个进程还在)时会降级为文件邮箱并继续工作,日志里会有 socket 通道启动失败(降级为文件邮箱);功能不中断,只是延迟变大 |
团队级排障与看门狗告警语义见 docs/team/WATCHDOG-RUNBOOK.md(告警类别与处置责任人)、
未验证项台账见 docs/team/iteration-backlog.md。
里程碑边界
| 里程碑 | 内容 | 状态 |
|---|---|---|
| M1 | 包骨架、/proactive 命令、工具接口、1s tick 与状态栏 |
已实现 |
| M2 | 空闲触发、投递与护栏闭环 | 已实现 |
| M3 | 任务落盘与调度 | 已实现 |
| M4 | 跨进程协作(注册表 / socket / 邮箱 / 去重 / 接线) | 已实现(测试级验证);V-03 核心闭环已合并态复测 |
| M5 | 协作闭环(逐条 reply / 线程 / 轮次上限) | 已实现(测试级验证);V-03 核心闭环已合并态复测 |
| M6 | 分发与文档(README / 包元数据 / 版本约束) | 已实现 |
| 跨进程端到端实测(V-03) | 两个真实 Pi 宿主跑通全链路 | 已完成(docs/review/cross-process-e2e.md);⚠️ 基线早于「agentId 会话稳定化」与 M5c 修复轮 → 合并态已复测部分场景(核心闭环与达上限通知),原 11 场景在合并态的逐条复测未做 |
| TUI 交互语义 | 真人按键下验证主动提问 | pty 仅抽验部分场景(docs/review/tui-smoke.md;7 项未验证,基线早于提问路径重写,需重跑才算数) |
| 跨家族独立复验 | 异族模型家族独立复审 | 未验证(本机只有 minimax 一个模型家族) |
| 跨机行为 | 两机 / NFS 实测 | 未验证(仅落盘排队 ~1s) |
| TUI 完整链路(提问→回答→回填) | pty 下的端到端提问闭环 | 未验证(pty 档只抽验了部分场景;7 项未验证,需重跑才算数) |
| 长时驻留(>10 分钟) | 长时间运行下的资源 / 孤儿进程 / 内存行为 | 未验证(实测每个宿主生命周期 ≤ 4 分钟) |
every 小时级节拍 |
小时级运行下的节拍漂移是否累积 | 未验证(短时实测节拍正常) |
| 门禁 G11(文档↔实现一致性断言) | 机器化的文档↔实现一致性门禁 | 已存在(判据在 scripts/merge-gate.sh 的 G11 段,默认零容忍;含反向断言:文档的否定式声明若指向仓库里已存在的对象 → 判为违规;回归自测在 scripts/tests/gate-doc-consistency.test.sh) |