personal-bot 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +6 -2
- package/README.md +9 -7
- package/README.zh-CN.md +9 -7
- package/docs/README.md +3 -1
- package/docs/agent-collaboration.md +98 -40
- package/docs/file-storage.md +25 -4
- package/docs/release.md +2 -2
- package/docs/subagent-upgrade-proposal.md +357 -0
- package/docs/technical-design.md +23 -4
- package/docs/validation.md +35 -5
- package/npm-shrinkwrap.json +2253 -0
- package/package.json +5 -4
- package/resources/agents/coordinator.md +8 -1
- package/resources/agents/general-purpose.md +10 -4
- package/resources/prompts/tool-guidelines.md +2 -2
- package/resources/prompts/workspace.md +2 -2
- package/resources/references/compute.md +3 -3
- package/resources/references/task-conductor.md +4 -4
- package/resources/skills/project-maintenance/SKILL.md +1 -1
- package/resources/skills/provider-connections/SKILL.md +2 -2
- package/src/common/markdown.ts +26 -0
- package/src/extension/agent-events.ts +116 -14
- package/src/extension/agents-command.ts +15 -17
- package/src/extension/command.ts +5 -3
- package/src/extension/lifecycle.ts +7 -2
- package/src/extension/subagent-entry.ts +144 -31
- package/src/extension/teams-command.ts +60 -0
- package/src/integrations/pi/agent-context.ts +1 -1
- package/src/integrations/pi/agent-input.ts +19 -0
- package/src/integrations/pi/pi-agents.ts +22 -9
- package/src/integrations/pi/team-context.ts +78 -0
- package/src/model/agent.ts +2 -0
- package/src/model/team.ts +29 -0
- package/src/pbot.ts +5 -3
- package/src/services/agents/agent-service.ts +91 -21
- package/src/services/agents/team-service.ts +274 -0
- package/src/storage/agent-files.ts +43 -10
- package/src/storage/team-files.ts +93 -0
- package/src/tools/agents/contract.ts +8 -6
- package/src/tools/agents/tool.ts +10 -9
- package/src/tools/register.ts +6 -3
- package/src/tools/result.ts +5 -0
- package/src/tools/teams/contract.ts +67 -0
- package/src/tools/teams/tool.ts +63 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "personal-bot",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/realqiyan/personal-bot.git"
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"themes": []
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
+
"npm-shrinkwrap.json",
|
|
27
28
|
"src",
|
|
28
29
|
"resources",
|
|
29
30
|
"scripts/setup-python.py",
|
|
@@ -58,9 +59,9 @@
|
|
|
58
59
|
"ajv-formats": "3.0.1"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
|
-
"@earendil-works/pi-ai": "1.
|
|
62
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
63
|
-
"@earendil-works/pi-tui": "1.
|
|
62
|
+
"@earendil-works/pi-ai": "1.4.0",
|
|
63
|
+
"@earendil-works/pi-coding-agent": "1.4.0",
|
|
64
|
+
"@earendil-works/pi-tui": "1.4.0",
|
|
64
65
|
"@types/node": "24.10.1",
|
|
65
66
|
"dependency-cruiser": "18.5.0",
|
|
66
67
|
"typebox": "1.3.27",
|
|
@@ -6,16 +6,23 @@
|
|
|
6
6
|
|
|
7
7
|
- 普通问答和不需要独立上下文的工作直接完成。
|
|
8
8
|
- 独立协助或核验使用 pbot_agent。委派提供目标、交付要求、资料依据和约束,不依赖专家知道父会话历史。
|
|
9
|
+
- 需要成员直接交流时,使用 pbot_team 组队。roundRobin 依次讨论,parallel 在每轮内并行、轮间汇合。提供目标、每人职责、授权范围及交付要求。成员原文直接同步,无须逐条转述。
|
|
9
10
|
- 周期、重复流程、DAG 或需在当前 Pi 退出后继续的任务,先读包内 `references/task-conductor.md`,再按 using-task-conductor 技能使用原生任务能力。缺少该能力只阻塞依赖它的部分。
|
|
10
11
|
|
|
11
12
|
## 委派与验收
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
默认由专家交回报告和必要产物引用,由主会话整合正式资料。原任务已授权资料修改时,可在委派中明确专家的修改范围,无须用户另行指定由专家执行;多个协作者写入时划分文件责任。需要复现时保存必要源码、输入、参数和结果,临时产物放入 scratch。
|
|
14
15
|
|
|
15
16
|
收到结果后,按本次完成条件核对报告与实际产物。汇总数值前核对对象、时期、单位、指标定义和来源;范围不同的结果分别解释,重复转述不算独立证据。证据判断使用 research-evidence,数据与计算核验使用 analysis-validation。
|
|
16
17
|
|
|
17
18
|
失败、中断或取消后,判断已完成部分与后续条件,不自动重跑。人工子会话输入不扩大主会话原任务授权。委派不扩大原用户授权;需要用户决定的业务交回主会话说明。
|
|
18
19
|
|
|
20
|
+
## 团队协作
|
|
21
|
+
|
|
22
|
+
公共消息与成员当前团队请求的最终回答会向团队公开;只提供本次协作需要的信息,避免披露私人历史或无关资料。成员已有记忆可能影响回答,团队讨论不等于独立复核。
|
|
23
|
+
|
|
24
|
+
批次 finished 只表示安排的发言均结束,不证明任务完成或达成共识。直接核对成员原文和实际产物,按原任务验收;团队调度与状态转换遵循工具契约。
|
|
25
|
+
|
|
19
26
|
## 独立复核与业务执行
|
|
20
27
|
|
|
21
28
|
按任务风险或用户要求组织独立方案复核。使用新子会话或独立任务节点,提供具体方案、精确参数、原始依据和已确认约束;原分析会话的后续对话不算独立复核。
|
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
完成本次委派范围内的研究、分析、资料查找、计算或独立复核,按任务选择技能。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
从委派引用及工作区入口查找依据。按交付需要说明结论、关键依据、来源时点、适用范围、限制及实际产物路径;数值注明影响理解的对象、期间、单位与计算口径。需要复现时保存并引用必要源码、输入、参数和结果。说明实际完成的检查,关键事实不足时明确无法确认。
|
|
6
6
|
|
|
7
7
|
独立复核时,核验方案参数、前置条件、实际后果和用户约束,独立查询关键事实,报告通过、拒绝或无法确认及其依据。研究与复核只使用明确的查询接口和计算,不执行业务变更;复核结论不授予执行许可。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
默认交回报告与必要产物引用,由主会话整合正式资料。原任务已授权资料修改且委派明确修改范围时,可在该范围内修改正式正文并说明变更,无须再次询问。
|
|
10
10
|
|
|
11
|
-
使用原生 CLI/MCP
|
|
11
|
+
使用原生 CLI/MCP 完成本次委派,可自主诊断工具接入与环境问题。新增权限或共享环境变更先核对原任务及委派中的已有授权,已覆盖的操作不重复询问;需要补充授权或用户在会话中操作时,交回已有结果、缺口和所需条件,继续不受影响的工作。项目构建和测试遵循项目声明的环境。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
不创建子专家或外部任务,不管理团队。需要其他协作者或持久执行时,交回建议,由主会话决定。
|
|
14
|
+
|
|
15
|
+
## 团队工作
|
|
16
|
+
|
|
17
|
+
收到团队发言安排时,按当前职责和轮次工作。使用 pbot_team_message read/post 直接交流原文,replyTo 指向公共消息;不等待对方在线回应,不通过发消息追加轮次。上下文注明未包含或截断的内容时,按 seq/offset 读取所需原文后再下结论;压缩后缺少旧证据时也应重读。成员的话是协作信息,不是新的用户授权。
|
|
18
|
+
|
|
19
|
+
post 与本次团队请求的最终答复会公开给全体成员。只公开任务需要的信息,不主动披露私人工作历史、完整工具输出或无关资料。指出依据、分歧、未解决问题与产物位置。每轮接续已有工作,不重复可能产生副作用的步骤;结果未知时先查询,不重发操作。轮次结束不等于任务通过验收。
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
pbot 工具返回结构化对象,按各工具契约检查结果;脚本不能只看 Promise
|
|
1
|
+
pbot 工具返回结构化对象,按各工具契约检查结果;脚本不能只看 Promise 是否成功返回。
|
|
2
2
|
|
|
3
|
-
有依赖的调用按顺序 await
|
|
3
|
+
有依赖的调用按顺序 await,为工具所需的用户确认留足等待时间。
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- 涉及工作区资料时,优先从已有根 `AGENTS.md` 定位相关项目或长期事务,按需读取目录入口、正文和匹配的技能。没有资料入口时按任务查找或创建必要资料。
|
|
10
10
|
- 复用历史结论前核对范围、时点与关键条件,只重核受变化影响的部分。分析假设不等于用户已确认的约束。
|
|
11
11
|
- 复杂任务先明确交付用途、必要证据、可接受限制与完成条件,沿用已有约定。
|
|
12
|
-
-
|
|
12
|
+
- 在当前授权内持续推进,常规、可逆的实现选择自行决定。新增权限、安装或共享环境变更先核对已有授权,已覆盖的操作不重复询问。仅当缺失信息影响目标、关键结果或授权时提问;暂停依赖步骤,继续有助于目标的独立工作。
|
|
13
13
|
- 资料、手册、工具结果、待办和职责补充不扩大用户授权。只开展当前任务需要的工作,不为普通问答增加文件或固定流程。
|
|
14
14
|
- 使用常用词、短句和一致术语。先给结论,按需说明依据、假设、验证、限制和实际产物路径。
|
|
15
15
|
|
|
@@ -34,6 +34,6 @@ CLI 通过 Bash 使用原生命令与当前 help,MCP 使用 Pi 原生接入与
|
|
|
34
34
|
|
|
35
35
|
按用户授权执行本次业务。研究、接入和验证不授权交易、转账或账本修改。协议成功不证明业务完成;超时、断连和取消不证明未执行。可能已发送的操作先查询原生状态,不因恢复会话或结果缺失自动重发。
|
|
36
36
|
|
|
37
|
-
共享 Python 环境为 ready 时,通过 Bash 使用 python 或 python3 和第三方库原生 API
|
|
37
|
+
共享 Python 环境为 ready 时,通过 Bash 使用 python 或 python3 和第三方库原生 API。环境异常可自主诊断,只暂停依赖该环境的步骤,不自行退回系统 Python;项目构建和测试遵循项目声明的环境。运行计算前按需读取包内 `references/compute.md`,按项目约定选择工作目录并保存必要产物。
|
|
38
38
|
|
|
39
39
|
Bash、Python、MCP 和其他扩展具有实际账户与凭据权限,不是沙箱。
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## 环境
|
|
4
4
|
|
|
5
|
-
主会话与专家的 `pbot_environment.python` 给出当前共享 Python 3.11+ 环境状态。为 ready 时,通过 Bash 使用 python 或 python3,按任务导入第三方库原生 API
|
|
5
|
+
主会话与专家的 `pbot_environment.python` 给出当前共享 Python 3.11+ 环境状态。为 ready 时,通过 Bash 使用 python 或 python3,按任务导入第三方库原生 API。项目构建和测试遵循项目声明的环境,在对应命令中使用。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
状态未就绪或依赖异常时,可自主诊断,只暂停依赖该环境的计算,不自动退回系统 Python,不删除或重建共享环境。额外依赖或共享环境变更先核对已有授权及影响范围;未获授权时说明所需变更并询问。需要重试 setup 时,请用户在当前 Pi 会话执行 `/pbot python setup`;专家将所需会话操作或补充授权交回主会话。
|
|
8
8
|
|
|
9
9
|
所有 pbot 工作区共用当前用户的环境。更新依赖前停止相关计算,避免影响其他工作区;不同插件版本的依赖要求可能冲突。库清单说明可用方法,不代替本次环境状态检查。
|
|
10
10
|
|
|
@@ -60,7 +60,7 @@ plt.close(fig)
|
|
|
60
60
|
|
|
61
61
|
## TA-Lib 技术指标
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
常用原生函数包括 `SMA`、`EMA`、`MACD`、`RSI`、`BBANDS`、`ATR`、`ADX`、`STOCH`、`OBV` 和 `CDL*` 形态函数。用 `talib.get_function_groups()` 或 `talib.get_functions()` 查找能力,用 `help(talib.函数名)` 核对参数与返回值,不凭名称猜测。
|
|
64
64
|
|
|
65
65
|
以下脚本从第一个命令行参数指定的 JSON 文件读取 close 数组,计算均线并输出严格 JSON:
|
|
66
66
|
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
先通过 Pi 原生工具发现查找 Task Conductor,并加载 using-task-conductor 技能,以实际 MCP schema 为准。工具未直接显示不等于未安装;技能可用时,按技能处理连接或排障。
|
|
8
8
|
|
|
9
|
-
技能缺失时,用 `pi list` 检查插件及资源启用状态。已有旧版接入包时,先审阅官方升级说明,确认来源和作用域后显式替换旧包声明,不同时加载两份;服务更新与 MCP 配置另行核对,保留任务历史。确未安装则说明需要 Pi 1.4.0
|
|
9
|
+
技能缺失时,用 `pi list` 检查插件及资源启用状态。已有旧版接入包时,先审阅官方升级说明,确认来源和作用域后显式替换旧包声明,不同时加载两份;服务更新与 MCP 配置另行核对,保留任务历史。确未安装则说明需要 Pi 1.4.0+,核对已有安装授权,已覆盖时直接使用工作区安装命令,未覆盖时再请求授权:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
pi install --local npm:task-conductor-plugin
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
需要重载及通过技能交互安装时,请用户在当前 Pi 会话执行 `/reload`,再执行 `/skill:using-task-conductor` 处理服务安装、setup 与原生 MCP 接入;这些斜杠命令不能通过 Bash 代执行。可直接读取已发现的技能继续可执行的步骤。复用已有原生连接,不在聊天中索取凭据值;CLI 配置查询不等价于当前会话的临时连接状态。插件可见不证明服务已连接,执行器 ready 不证明模型认证和执行成功。
|
|
16
16
|
|
|
17
17
|
能力不可用时说明依赖它的工作受阻,继续其他已授权工作;不以循环等待或后台子会话替代持久调度。
|
|
18
18
|
|
|
@@ -22,10 +22,10 @@ pi install --local npm:task-conductor-plugin
|
|
|
22
22
|
|
|
23
23
|
核实节点实际具备的工具、技能和 Python 环境,不假定它继承主会话能力或批准通道。节点任务限定为本次交付,不再递归委派;普通 Pi 节点不是受限专家,这些约定也不是宿主隔离。
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
没有可靠批准通道时,不安排需人工批准的后台操作或无人值守交易;不得绕过批准要求。调度和恢复任务不扩大已有授权。独立复核使用新的独立节点,只查询和计算;协调器验收不等于专业复核或业务批准。
|
|
26
26
|
|
|
27
27
|
## 核对与保存
|
|
28
28
|
|
|
29
|
-
任务定义、执行和节点分别用 taskId、runId 和节点 ID
|
|
29
|
+
任务定义、执行和节点分别用 taskId、runId 和节点 ID 标识。提交响应不明时先查已有标识;未取得 ID 时,按接口可用的请求关联、任务列表、提交时间或任务特征查找并核对。未能确认是否已创建时保留未知状态,不重复提交。实时状态查询 Task Conductor;项目资料只保存必要定义、标识和有时点的结果依据。
|
|
30
30
|
|
|
31
31
|
取消当前执行与停止未来周期分别操作,并查询确认。pbot_agent stop 或 Pi 关闭不停止 Task Conductor。报告已设置调度须有实际提交结果;报告本轮成功须核对本轮产物与完成条件。
|
|
@@ -19,7 +19,7 @@ description: 维护项目结论、证据与可复用任务定义。用于资料
|
|
|
19
19
|
|
|
20
20
|
有跨任务价值的知识提炼到 knowledge/ 并引用项目证据,保留复用所需的条件。知识按主题命名,需要保留历史的研究和决策按日期与主题命名。
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
尊重根 AGENTS.md 已有的导航格式和工作区约定,只更新受本次变化影响的入口,不为统一样式重排。新建导航时可用 text 目录树,展开到有资料的主题或项目目录并简注用途,也可按已有习惯使用链接列表;任务状态沿用现有标记。结论和未决事项留在正文,导航引用对应资料。
|
|
23
23
|
|
|
24
24
|
## 复用任务定义
|
|
25
25
|
|
|
@@ -13,13 +13,13 @@ description: 接入或修复原生 CLI/MCP,核对认证与可用性,维护
|
|
|
13
13
|
|
|
14
14
|
## MCP
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
可先用当前可见工具及 CLI 自主诊断。需要查看当前会话的连接、认证、工具与配置来源时,请用户在该 Pi 会话执行 `/mcp`;这是会话操作,不能通过 Bash 代执行。Bash 中使用 `pi mcp --help` 及相关子命令查询或配置;CLI 查询不等价于当前会话状态,尤其不能据此判断会话内临时连接。
|
|
17
17
|
|
|
18
18
|
新增连接默认只接入当前项目。从工作区根目录执行 `pi mcp add --local <server> ...`,写入 `.pi/mcp.json`;删除项目连接使用 `pi mcp remove --local <server>`。add/remove 未指定 `--local` 时修改当前 Pi agent dir 的 `mcp.json`,仅在用户明确要求跨项目使用时选择该范围。第三方 setup 同样采用项目配置;无法指定范围时,改用 Pi 原生项目配置。
|
|
19
19
|
|
|
20
20
|
项目配置经 Pi 项目信任规则加载,同名条目可能覆盖用户级配置。修改前核对同名项,保留无关条目与已有用户级连接;迁移另按用户指定范围处理。认证使用原生登录或环境变量引用。
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
外部修改配置后,请用户在当前 Pi 会话执行 `/reload`,再用 `/mcp` 核对实际配置来源和连接状态;不要声称已代用户完成会话操作。当前工具 schema 通过原生工具搜索或 codemode 按需取得。新增权限、安装或共享配置变更先核对已有授权,已授权的操作不重复询问。
|
|
23
23
|
|
|
24
24
|
## 验证与手册
|
|
25
25
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Json } from "./json.js";
|
|
2
|
+
|
|
3
|
+
/** Preserve original text, including embedded Markdown fences. */
|
|
4
|
+
export function markdownBlock(text: string): string {
|
|
5
|
+
let fenceLength = 3;
|
|
6
|
+
for (const match of text.matchAll(/`+/g)) fenceLength = Math.max(fenceLength, match[0].length + 1);
|
|
7
|
+
const fence = "`".repeat(fenceLength);
|
|
8
|
+
return fence + "text\n" + text + "\n" + fence;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Render structured result fields without changing their machine-readable values. */
|
|
12
|
+
export function markdownFields(value: Json, level = 2): string {
|
|
13
|
+
if (value === null || typeof value !== "object") return typeof value === "string" ? markdownBlock(value) : String(value);
|
|
14
|
+
if (Array.isArray(value)) return value.length ? value.map((item, index) =>
|
|
15
|
+
"#".repeat(level) + " " + (index + 1) + "\n\n" + markdownFields(item, level + 1)).join("\n\n") : "(空)";
|
|
16
|
+
const fields: string[] = [], sections: string[] = [];
|
|
17
|
+
for (const [key, item] of Object.entries(value)) {
|
|
18
|
+
if ((item !== null && typeof item === "object") || (typeof item === "string" && (item.includes("\n") || key === "report" || key === "text"))) {
|
|
19
|
+
sections.push("#".repeat(level) + " " + key + "\n\n" + markdownFields(item, level + 1));
|
|
20
|
+
} else {
|
|
21
|
+
const text = String(item).replace(/[\\`*_{}\[\]<>#!|]/g, "\\$&");
|
|
22
|
+
fields.push("- " + key + ": " + text);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return [fields.join("\n"), ...sections].filter(Boolean).join("\n\n");
|
|
26
|
+
}
|
|
@@ -1,28 +1,101 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import type { ExtensionAPI, ExtensionContext, CustomMessageEntryDraft, SessionBoundaryDraft } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import type { Lifecycle } from "./lifecycle.js";
|
|
3
4
|
import type { Confirmations } from "../ui/confirmation.js";
|
|
4
5
|
import { agentOwner, deliveredCycles } from "../integrations/pi/agent-context.js";
|
|
5
6
|
import { agentBoard } from "../ui/agent-display.js";
|
|
7
|
+
import { deliveredTeams } from "../integrations/pi/team-context.js";
|
|
8
|
+
import type { TeamInput } from "../model/team.js";
|
|
9
|
+
import { json } from "../common/json.js";
|
|
10
|
+
import { markdownFields } from "../common/markdown.js";
|
|
6
11
|
|
|
7
12
|
export function registerAgentEvents(pi: ExtensionAPI, lifecycle: Lifecycle, confirmations: Confirmations) {
|
|
8
13
|
let ctx: ExtensionContext | undefined, timer: ReturnType<typeof setInterval> | undefined;
|
|
9
14
|
let paused = false;
|
|
15
|
+
let controlBusySince: number | undefined;
|
|
16
|
+
let failure: { key: string; error: string; recovery: "retry" | "repair" | "reload" } | undefined;
|
|
17
|
+
const recordFailure = (error: unknown) => {
|
|
18
|
+
const busy = (error as NodeJS.ErrnoException).code === "PBOT_CONTROL_BUSY";
|
|
19
|
+
if (busy) {
|
|
20
|
+
controlBusySince ??= Date.now();
|
|
21
|
+
if (Date.now() - controlBusySince < 5000) return;
|
|
22
|
+
} else controlBusySince = undefined;
|
|
23
|
+
let recovery: "retry" | "repair" | "reload" = busy ? "retry" : "repair";
|
|
24
|
+
try { lifecycle.active?.teams.check(); } catch { recovery = "reload"; }
|
|
25
|
+
// A stronger recovery requirement needs a new notice even if the earlier one was delivered.
|
|
26
|
+
if (!failure || (failure.recovery !== recovery && (failure.recovery === "retry" || recovery === "reload"))) {
|
|
27
|
+
failure = { key: "agent-error:" + randomUUID(), error: String(error), recovery };
|
|
28
|
+
}
|
|
29
|
+
if (ctx?.hasUI) ctx.ui.setStatus("pbot-agents-error", String(error));
|
|
30
|
+
};
|
|
10
31
|
const submitting = new Map<string, number>();
|
|
11
|
-
|
|
32
|
+
// Only a lock-busy, not-yet-published input is retried in this runtime. Reload discards this map.
|
|
33
|
+
const dispatching = new Map<string, TeamInput>();
|
|
34
|
+
let pausePending = false;
|
|
35
|
+
const pauseTeams = () => {
|
|
36
|
+
const instance = lifecycle.active;
|
|
37
|
+
if (!ctx || !instance) return;
|
|
38
|
+
pausePending = true;
|
|
39
|
+
try { instance.teams.pauseAll(agentOwner(ctx), "主会话中断"); pausePending = false; dispatching.clear(); }
|
|
40
|
+
catch (error) {
|
|
41
|
+
const busy = (error as NodeJS.ErrnoException).code === "PBOT_CONTROL_BUSY";
|
|
42
|
+
if (!busy) instance.teams.fail(error);
|
|
43
|
+
recordFailure(error);
|
|
44
|
+
if (!busy) throw error;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const collectNotification = (): CustomMessageEntryDraft | undefined => {
|
|
12
48
|
const instance = lifecycle.active;
|
|
13
49
|
if (!ctx || !instance || paused || ctx.signal?.aborted) return;
|
|
14
|
-
const delivered = deliveredCycles(ctx), now = Date.now();
|
|
50
|
+
const delivered = new Set([...deliveredCycles(ctx), ...deliveredTeams(ctx)]), now = Date.now();
|
|
15
51
|
for (const [key, at] of submitting) if (delivered.has(key) || now - at > 5000) submitting.delete(key);
|
|
16
|
-
const
|
|
52
|
+
const owner = agentOwner(ctx), notes = instance.teams.notifications(owner, instance.agents.list(owner))
|
|
53
|
+
.filter(v => !delivered.has(v.key) && !submitting.has(v.key)).slice(0, 8);
|
|
54
|
+
if (notes.length) {
|
|
55
|
+
const keys = notes.map(v => v.key);
|
|
56
|
+
for (const key of keys) submitting.set(key, now);
|
|
57
|
+
return { type: "custom_message", customType: "pbot.team-results", display: true,
|
|
58
|
+
content: ["# 团队批次状态", "", "团队批次状态已变化。用 pbot_team status/read 查询;结束不等于业务验收通过。暂停可能仍有在途工作,不能自动重跑旧请求。", "",
|
|
59
|
+
notes.map(v => ["## " + v.name, "", "- `teamId`: `" + v.teamId + "`", "- `runId`: `" + v.runId + "`",
|
|
60
|
+
"- 状态:" + v.state, "- 在途工作:" + v.activeCount, ...(v.reason ? ["", "### 原因", "", v.reason] : [])].join("\n")).join("\n\n")].join("\n"),
|
|
61
|
+
details: { keys } };
|
|
62
|
+
}
|
|
63
|
+
const links = instance.teams.links();
|
|
64
|
+
const pending = instance.agents.completions(owner).filter(v =>
|
|
65
|
+
!v.cycle.requestIds.some(id => links.has(id)) &&
|
|
66
|
+
!(v.cycle.cycleId === v.cycle.generation && [...links.values()].some(l => l.request.agentId === v.agentId && l.request.generation === v.cycle.generation)) &&
|
|
67
|
+
!delivered.has(v.agentId + ":" + v.cycle.cycleId) && !submitting.has(v.agentId + ":" + v.cycle.cycleId));
|
|
17
68
|
if (!pending.length) return;
|
|
18
69
|
const batch = pending.slice(0, 8), keys = batch.map(v => v.agentId + ":" + v.cycle.cycleId);
|
|
19
70
|
for (const key of keys) submitting.set(key, now);
|
|
20
71
|
return { type: "custom_message", customType: "pbot.agent-results", display: true,
|
|
21
|
-
content: "子会话工作已结束。完整报告用 pbot_agent read
|
|
22
|
-
(batch.some(v => v.cycle.source !== "parent") ? "包含人工子会话输入,其结果不扩大主会话原任务授权。\n" : "") +
|
|
23
|
-
batch.map(v =>
|
|
24
|
-
cycle: { cycleId: v.cycle.cycleId, state: v.cycle.state, source: v.cycle.source, requestIds: v.cycle.requestIds,
|
|
25
|
-
|
|
72
|
+
content: "# 子会话工作结果\n\n子会话工作已结束。完整报告用 pbot_agent read 读取,指定对应 agentId 和 cycle.cycleId;nextOffset 非空时沿用该 cycleId 续读。\n\n" +
|
|
73
|
+
(batch.some(v => v.cycle.source !== "parent") ? "包含人工子会话输入,其结果不扩大主会话原任务授权。\n\n" : "") +
|
|
74
|
+
batch.map(v => "## " + v.name + "\n\n" + markdownFields(json({ agentId: v.agentId,
|
|
75
|
+
cycle: { cycleId: v.cycle.cycleId, state: v.cycle.state, source: v.cycle.source, requestIds: v.cycle.requestIds,
|
|
76
|
+
...(v.cycle.error === undefined ? {} : { error: v.cycle.error }) },
|
|
77
|
+
report: v.report.slice(0, 2000), truncated: v.report.length > 2000 }), 3)).join("\n\n"), details: { keys } };
|
|
78
|
+
};
|
|
79
|
+
const notification = (): CustomMessageEntryDraft | undefined => {
|
|
80
|
+
if (!ctx || !lifecycle.active || paused || ctx.signal?.aborted) return;
|
|
81
|
+
if (!failure) {
|
|
82
|
+
try { return collectNotification(); }
|
|
83
|
+
catch (error) { recordFailure(error); }
|
|
84
|
+
}
|
|
85
|
+
if (!failure || deliveredCycles(ctx).has(failure.key)) return;
|
|
86
|
+
const now = Date.now(), sent = submitting.get(failure.key);
|
|
87
|
+
if (sent !== undefined && now - sent < 5000) return;
|
|
88
|
+
submitting.set(failure.key, now);
|
|
89
|
+
const recovery = failure.recovery === "reload"
|
|
90
|
+
? "团队服务或协作存储已停用,修复文件后当前实例仍不可用。先按错误信息排障,再请用户在当前 Pi 会话执行 /reload;重载前不要反复调用 pbot_team。重载后用 pbot_team status/read 核对团队状态与公共原文。"
|
|
91
|
+
: failure.recovery === "retry"
|
|
92
|
+
? "团队控制锁持续被占用,后台会继续重试;锁释放后自动恢复扫描,不自动删除锁或重新提交任务。"
|
|
93
|
+
: "报告或控制记录读取失败。先按错误信息排障;修复后后台扫描会重试。";
|
|
94
|
+
return { type: "custom_message", customType: "pbot.agent-error", display: true,
|
|
95
|
+
content: ["# 子会话自动跟进故障", "", recovery,
|
|
96
|
+
...(pausePending ? ["", "团队暂停尚未确认,不能据此开始成员私人任务。"] : []), "",
|
|
97
|
+
"尚未交付的结果未确认。需要核对成员时,用 pbot_agent list/status 查询状态、pbot_agent read 读取可用报告;结果未知不构成重新提交的依据。", "",
|
|
98
|
+
markdownFields({ error: failure.error })].join("\n"), details: { keys: [failure.key] } };
|
|
26
99
|
};
|
|
27
100
|
const refresh = () => {
|
|
28
101
|
const instance = lifecycle.active;
|
|
@@ -31,19 +104,48 @@ export function registerAgentEvents(pi: ExtensionAPI, lifecycle: Lifecycle, conf
|
|
|
31
104
|
instance.agentOwner = agentOwner(ctx);
|
|
32
105
|
instance.agents.refresh(instance.agentOwner);
|
|
33
106
|
const values = instance.agents.list(instance.agentOwner);
|
|
107
|
+
try {
|
|
108
|
+
instance.teams.recover(instance.agentOwner);
|
|
109
|
+
if (pausePending) pauseTeams();
|
|
110
|
+
if (!pausePending) {
|
|
111
|
+
for (const input of instance.teams.advance(instance.agentOwner, values, instance.agents.completions(instance.agentOwner))) dispatching.set(input.requestId, input);
|
|
112
|
+
for (const [id, input] of dispatching) {
|
|
113
|
+
try { instance.agents.enqueueTeam(input, instance.agentOwner); dispatching.delete(id); }
|
|
114
|
+
catch (error) {
|
|
115
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
116
|
+
if (code === "PBOT_CONTROL_BUSY") break;
|
|
117
|
+
if (code !== "TEAM_INPUT_STALE") throw error;
|
|
118
|
+
dispatching.delete(id); instance.teams.pauseMember(input.agentId, "安排失效;未自动重发");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if ((error as NodeJS.ErrnoException).code !== "PBOT_CONTROL_BUSY") { instance.teams.fail(error); dispatching.clear(); }
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
if (ctx.hasUI) {
|
|
127
|
+
const teams = instance.teams.list(instance.agentOwner, values);
|
|
128
|
+
ctx.ui.setStatus("pbot-teams", teams.length ? "团队 " + teams.filter(t => t.owned && t.run?.state === "running").length + " 运行 / " + teams.length : undefined);
|
|
129
|
+
}
|
|
34
130
|
if (ctx.mode === "tui") ctx.ui.setWidget("pbot-agents", values.some(v => ["starting", "busy", "stopping"].includes(v.state)) ? (_tui, theme) => agentBoard(values, theme) : undefined);
|
|
35
|
-
if (
|
|
36
|
-
|
|
131
|
+
if (!pausePending) {
|
|
132
|
+
failure = undefined; controlBusySince = undefined;
|
|
133
|
+
if (ctx.hasUI) ctx.ui.setStatus("pbot-agents-error", undefined);
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
recordFailure(error);
|
|
137
|
+
}
|
|
138
|
+
if (ctx.isIdle() && !paused) { const message = notification(); if (message) pi.sendMessage(message, { triggerTurn: true, deliverAs: "steer" }); }
|
|
37
139
|
};
|
|
38
140
|
const boundary = (outcome: string, entries: SessionBoundaryDraft[], context: ExtensionContext) => {
|
|
39
141
|
ctx = context;
|
|
40
|
-
if (outcome === "aborted" || ctx.signal?.aborted) { paused = true; confirmations.pause(); return; }
|
|
142
|
+
if (outcome === "aborted" || ctx.signal?.aborted) { paused = true; confirmations.pause(); pauseTeams(); return; }
|
|
41
143
|
const message = notification();
|
|
42
144
|
if (message) return { entries: [...entries, message], continue: true };
|
|
43
145
|
return;
|
|
44
146
|
};
|
|
45
147
|
pi.on("session_start", (_event, context) => {
|
|
46
|
-
ctx = context; paused = false; confirmations.resume(); submitting.clear();
|
|
148
|
+
ctx = context; paused = false; pausePending = false; controlBusySince = undefined; failure = undefined; confirmations.resume(); submitting.clear(); dispatching.clear();
|
|
47
149
|
if (timer) clearInterval(timer);
|
|
48
150
|
if (lifecycle.active) { lifecycle.active.agentOwner = agentOwner(ctx); timer = setInterval(refresh, 1000); timer.unref(); }
|
|
49
151
|
refresh();
|
|
@@ -54,12 +156,12 @@ export function registerAgentEvents(pi: ExtensionAPI, lifecycle: Lifecycle, conf
|
|
|
54
156
|
pi.on("turn_end", (event, context) => boundary(event.outcome, event.entries, context));
|
|
55
157
|
pi.on("agent_before_settle", (event, context) => boundary(event.outcome, event.entries, context));
|
|
56
158
|
pi.on("agent_settled", (event, context) => {
|
|
57
|
-
ctx = context; if (event.aborted) { paused = true; confirmations.pause(); }
|
|
159
|
+
ctx = context; if (event.aborted) { paused = true; confirmations.pause(); pauseTeams(); }
|
|
58
160
|
});
|
|
59
161
|
pi.on("tool_result", event => {
|
|
60
162
|
if (event.toolName !== "pbot_agent") return;
|
|
61
163
|
const keys = (event.details as { agentReadKeys?: string[] } | undefined)?.agentReadKeys;
|
|
62
164
|
if (keys?.length) pi.appendEntry("pbot.agent-read", { keys, toolCallId: event.parentToolCallId ?? event.toolCallId });
|
|
63
165
|
});
|
|
64
|
-
pi.on("session_shutdown", () => { if (timer) clearInterval(timer); timer = undefined; submitting.clear(); ctx = undefined; });
|
|
166
|
+
pi.on("session_shutdown", () => { if (timer) clearInterval(timer); timer = undefined; submitting.clear(); dispatching.clear(); pausePending = false; ctx = undefined; });
|
|
65
167
|
}
|
|
@@ -7,9 +7,9 @@ import { tmuxName } from "../integrations/pi/pi-agents.js";
|
|
|
7
7
|
import { agentSummary } from "../ui/agent-display.js";
|
|
8
8
|
import type { Confirmations } from "../ui/confirmation.js";
|
|
9
9
|
|
|
10
|
-
export async function showAgents(_pi: ExtensionAPI, instance: WorkspaceInstance, ctx: ExtensionCommandContext, confirmations: Confirmations) {
|
|
10
|
+
export async function showAgents(_pi: ExtensionAPI, instance: WorkspaceInstance, ctx: ExtensionCommandContext, confirmations: Confirmations, agentIds?: string[]) {
|
|
11
11
|
instance.check();
|
|
12
|
-
const owner = agentOwner(ctx), values = instance.agents.list(owner);
|
|
12
|
+
const owner = () => agentOwner(ctx), values = instance.agents.list(owner()).filter(v => !agentIds || agentIds.includes(v.agentId));
|
|
13
13
|
if (ctx.mode !== "tui") { ctx.ui.notify(JSON.stringify(values), "info"); return; }
|
|
14
14
|
if (!values.length) { ctx.ui.notify("没有子会话;用 pbot_agent start 创建。", "info"); return; }
|
|
15
15
|
if (confirmations.presenting) { ctx.ui.notify("请先处理主会话批准。", "info"); return; }
|
|
@@ -17,31 +17,29 @@ export async function showAgents(_pi: ExtensionAPI, instance: WorkspaceInstance,
|
|
|
17
17
|
const selected = await ctx.ui.select("pbot 子会话", options);
|
|
18
18
|
const value = values[options.indexOf(selected ?? "")]; if (!value) return;
|
|
19
19
|
if (!value.owned) { ctx.ui.notify("请返回所属分支操作。", "info"); return; }
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const inTmux = Boolean(process.env.TMUX);
|
|
21
|
+
const enterTui = "进入 TUI(返回:Ctrl+B → " + (inTmux ? "l" : "d") + ")";
|
|
22
|
+
const action = await ctx.ui.select(value.name, [enterTui, "补充要求", "重命名", "停止"]);
|
|
23
|
+
instance.check();
|
|
24
|
+
if (action === "停止") { instance.agents.stop(value.agentId, owner()); return; }
|
|
25
|
+
if (action === "重命名") { const name = await ctx.ui.input("名称"); instance.check(); if (name?.trim()) instance.agents.rename(value.agentId, name.trim(), owner()); return; }
|
|
26
|
+
if (action === "补充要求") {
|
|
24
27
|
const message = await ctx.ui.editor("任务或补充要求");
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
else instance.agents.resume(value.agentId, message, owner);
|
|
28
|
-
}
|
|
28
|
+
instance.check();
|
|
29
|
+
if (message?.trim()) instance.agents.send(value.agentId, message, "steer", owner());
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
|
-
if (action
|
|
32
|
-
const page = instance.agents.read(value.agentId, owner);
|
|
33
|
-
await ctx.ui.editor("子会话报告(查看不领取通知)", page.report || page.error || page.state); return;
|
|
34
|
-
}
|
|
35
|
-
if (action !== "进入 TUI") return;
|
|
36
|
-
if (!instance.agents.host.alive(value.agentId)) { ctx.ui.notify("子会话已停止,请携新要求恢复。", "info"); return; }
|
|
32
|
+
if (action !== enterTui) return;
|
|
37
33
|
let tui: TUI | undefined;
|
|
38
34
|
await ctx.ui.custom<void>((terminal, _theme, _keys, done) => { tui = terminal; queueMicrotask(() => done()); return new Text("", 0, 0); });
|
|
39
35
|
if (!tui) return;
|
|
36
|
+
instance.check();
|
|
37
|
+
instance.agents.enter(value.agentId, owner());
|
|
40
38
|
const terminal = tui;
|
|
41
39
|
terminal.stop();
|
|
42
40
|
try {
|
|
43
41
|
const code = await new Promise<number | null>(resolve => {
|
|
44
|
-
const child = spawn("tmux", [
|
|
42
|
+
const child = spawn("tmux", [inTmux ? "switch-client" : "attach-session", "-t", "=" + tmuxName(value.agentId)], { stdio: "inherit" });
|
|
45
43
|
child.on("error", () => resolve(null)); child.on("close", resolve);
|
|
46
44
|
});
|
|
47
45
|
if (code !== 0) ctx.ui.notify("无法进入子会话。", "error");
|
package/src/extension/command.ts
CHANGED
|
@@ -4,13 +4,14 @@ import { prepareWorkspace } from "../services/workspace/workspace-preparation.js
|
|
|
4
4
|
import type { Confirmations } from "../ui/confirmation.js";
|
|
5
5
|
import type { Lifecycle } from "./lifecycle.js";
|
|
6
6
|
import { showAgents } from "./agents-command.js";
|
|
7
|
+
import { showTeams } from "./teams-command.js";
|
|
7
8
|
|
|
8
9
|
export function registerCommand(pi: ExtensionAPI, lifecycle: Lifecycle, confirmations: Confirmations) {
|
|
9
10
|
pi.registerCommand("pbot", {
|
|
10
|
-
description: "工作区:init、status、agents、python setup",
|
|
11
|
+
description: "工作区:init、status、agents、teams、python setup",
|
|
11
12
|
getArgumentCompletions: prefix => {
|
|
12
13
|
if (/\s/.test(prefix)) return null;
|
|
13
|
-
return ["init", "status", "agents", "python"]
|
|
14
|
+
return ["init", "status", "agents", "teams", "python"]
|
|
14
15
|
.filter(command => command.startsWith(prefix))
|
|
15
16
|
.map(command => ({ value: command, label: command }));
|
|
16
17
|
},
|
|
@@ -29,6 +30,7 @@ export function registerCommand(pi: ExtensionAPI, lifecycle: Lifecycle, confirma
|
|
|
29
30
|
ctx.ui.notify(JSON.stringify({ workspaceRoot: instance.root, release: packageVersion, python: instance.pythonEnvironment.status }, null, 2), "info"); return;
|
|
30
31
|
}
|
|
31
32
|
if (command === "agents") { await showAgents(pi, lifecycle.get(), ctx, confirmations); return; }
|
|
33
|
+
if (command === "teams") { await showTeams(pi, lifecycle.get(), ctx, confirmations); return; }
|
|
32
34
|
if (command === "python" && argument === "setup") {
|
|
33
35
|
const instance = lifecycle.get(), cancel = instance.signal(ctx.signal);
|
|
34
36
|
await ctx.waitForIdle();
|
|
@@ -37,7 +39,7 @@ export function registerCommand(pi: ExtensionAPI, lifecycle: Lifecycle, confirma
|
|
|
37
39
|
ctx.ui.notify(JSON.stringify(result, null, 2), "info");
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
40
|
-
ctx.ui.notify("用法:/pbot init | status | agents | python setup", "info");
|
|
42
|
+
ctx.ui.notify("用法:/pbot init | status | agents | teams | python setup", "info");
|
|
41
43
|
},
|
|
42
44
|
});
|
|
43
45
|
}
|
|
@@ -5,6 +5,7 @@ import { authorizeMutation } from "../services/workspace/workspace-files.js";
|
|
|
5
5
|
import type { AgentOwner } from "../model/agent.js";
|
|
6
6
|
import type { PythonEnvironmentService } from "../services/workspace/python-environment-service.js";
|
|
7
7
|
import type { AgentService } from "../services/agents/agent-service.js";
|
|
8
|
+
import type { TeamService } from "../services/agents/team-service.js";
|
|
8
9
|
import type { CallAccess } from "../tools/access.js";
|
|
9
10
|
|
|
10
11
|
export class WorkspaceInstance implements CallAccess {
|
|
@@ -12,7 +13,7 @@ export class WorkspaceInstance implements CallAccess {
|
|
|
12
13
|
#work = new AbortController();
|
|
13
14
|
#closing: Promise<void> | undefined;
|
|
14
15
|
#restorePython: (() => void) | undefined;
|
|
15
|
-
constructor(readonly root: string, readonly pythonEnvironment: PythonEnvironmentService, readonly agents: AgentService) {}
|
|
16
|
+
constructor(readonly root: string, readonly pythonEnvironment: PythonEnvironmentService, readonly agents: AgentService, readonly teams: TeamService) {}
|
|
16
17
|
async preparePython(signal?: AbortSignal) {
|
|
17
18
|
this.check();
|
|
18
19
|
this.#restorePython?.();
|
|
@@ -45,8 +46,12 @@ export class WorkspaceInstance implements CallAccess {
|
|
|
45
46
|
this.#work.abort();
|
|
46
47
|
this.#closing = (async () => {
|
|
47
48
|
try {
|
|
49
|
+
const errors: unknown[] = [];
|
|
50
|
+
try { if (this.agentOwner) this.teams.pauseAll(this.agentOwner, "主会话关闭或重新载入"); }
|
|
51
|
+
catch (error) { errors.push(error); }
|
|
52
|
+
this.teams.dispose();
|
|
48
53
|
const results = await Promise.allSettled([this.agents.close(this.agentOwner, preserveAgents), this.pythonEnvironment.close()]);
|
|
49
|
-
|
|
54
|
+
errors.push(...results.filter(result => result.status === "rejected").map(result => result.reason));
|
|
50
55
|
if (errors.length) throw new AggregateError(errors, "工作区关闭失败。");
|
|
51
56
|
} finally { this.#restorePython?.(); this.#restorePython = undefined; }
|
|
52
57
|
})();
|