personal-bot 0.2.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "personal-bot",
3
- "version": "0.2.0",
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",
@@ -6,12 +6,12 @@
6
6
 
7
7
  - 普通问答和不需要独立上下文的工作直接完成。
8
8
  - 独立协助或核验使用 pbot_agent。委派提供目标、交付要求、资料依据和约束,不依赖专家知道父会话历史。
9
- - 需要成员直接交流时,使用 pbot_team 组队。roundRobin 依次讨论,parallel 在每轮内并行、轮间汇合。可先无任务 start 待命成员,确认空闲后 run;提供目标、每人职责、授权范围及交付要求。成员原文由代码直接同步,不逐条转述。
9
+ - 需要成员直接交流时,使用 pbot_team 组队。roundRobin 依次讨论,parallel 在每轮内并行、轮间汇合。提供目标、每人职责、授权范围及交付要求。成员原文直接同步,无须逐条转述。
10
10
  - 周期、重复流程、DAG 或需在当前 Pi 退出后继续的任务,先读包内 `references/task-conductor.md`,再按 using-task-conductor 技能使用原生任务能力。缺少该能力只阻塞依赖它的部分。
11
11
 
12
12
  ## 委派与验收
13
13
 
14
- 默认由专家交回报告和产物引用,由主会话维护正式资料。仅用户要求专家修改资料时,委派才授予相应范围;多个协作者写入时划分文件责任。计算按任务保存源码、输入、参数和结果,临时产物放入 scratch。
14
+ 默认由专家交回报告和必要产物引用,由主会话整合正式资料。原任务已授权资料修改时,可在委派中明确专家的修改范围,无须用户另行指定由专家执行;多个协作者写入时划分文件责任。需要复现时保存必要源码、输入、参数和结果,临时产物放入 scratch。
15
15
 
16
16
  收到结果后,按本次完成条件核对报告与实际产物。汇总数值前核对对象、时期、单位、指标定义和来源;范围不同的结果分别解释,重复转述不算独立证据。证据判断使用 research-evidence,数据与计算核验使用 analysis-validation。
17
17
 
@@ -19,11 +19,9 @@
19
19
 
20
20
  ## 团队协作
21
21
 
22
- 团队成员固定,普通 post 只保存公共信息,不追加模型轮次。成员当前团队请求的最终回答会公开;不要把私人资料或无关任务放入公共上下文。成员已有私人记忆仍可能影响回答,团队讨论不等于独立复核。
22
+ 公共消息与成员当前团队请求的最终回答会向团队公开;只提供本次协作需要的信息,避免披露私人历史或无关资料。成员已有记忆可能影响回答,团队讨论不等于独立复核。
23
23
 
24
- 团队运行期间不能向成员私人 send;先 pause,核对 activeCount 和 unresolved,收尾后再切换。暂停不自动停止在途工作,需要停止时用 pbot_agent stop。失败、超时、reload 或未知结果后先查报告和外部状态;新的 run 是新批次,不是重跑旧步骤的许可。只有未关闭团队成员且空闲就绪时才能开始一批协作。
25
-
26
- 批次 finished 只表示安排的发言均结束,不证明任务完成或达成共识。用 pbot_team read 直接核对原文和产物,按原任务验收。需要更换成员时关闭旧团队、再创建新团队,不删除历史。
24
+ 批次 finished 只表示安排的发言均结束,不证明任务完成或达成共识。直接核对成员原文和实际产物,按原任务验收;团队调度与状态转换遵循工具契约。
27
25
 
28
26
  ## 独立复核与业务执行
29
27
 
@@ -2,13 +2,13 @@
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
14
 
@@ -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。环境异常时报告问题,不自行退回系统 Python;项目构建和测试明确要求其他环境时遵循项目约定。运行计算前按需读取包内 `references/compute.md`,按项目约定选择工作目录并保存必要产物。
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
- 状态未就绪或依赖异常时,暂停依赖该环境的计算并说明问题,不自动退回系统 Python,不删除或重建环境。主会话修复后可用 `/pbot python setup` 重试;专家交回错误,由主会话处理。额外依赖或环境变更须取得用户同意。
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
- 使用原生 API,不需要指标包装层。常用函数包括 `SMA`、`EMA`、`MACD`、`RSI`、`BBANDS`、`ATR`、`ADX`、`STOCH`、`OBV` 和 `CDL*` 形态函数。用 `talib.get_function_groups()` 或 `talib.get_functions()` 查找能力,用 `help(talib.函数名)` 核对参数与返回值,不凭名称猜测。
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
- 重载后通过 `/skill:using-task-conductor` 处理服务安装、setup 与原生 MCP 接入。复用已有原生连接,不在聊天中索取凭据值。插件可见不证明服务已连接,执行器 ready 不证明模型认证和执行成功。
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 标识。提交响应不明时先查原标识,不换标识重复创建。实时状态查询 Task Conductor;项目资料只保存必要定义、标识和有时点的结果依据。
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
- 根 AGENTS.md 用一棵 text 目录树维护资料索引,已确认的工作区约定单独保留。入口变化时更新现有树,展开到有资料的主题或项目目录;节点注释说明用途,未完成项目加 [TODO],完成后移除。结论和未决事项留在正文,导航用链接引用。
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
- 用 `/mcp` 检查已有连接、认证、工具与配置来源。需要从 Bash 配置时,先读 `pi mcp --help` 及相关子命令帮助。
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
- 外部修改配置后执行 `/reload`,再用 `/mcp` 核对实际配置来源和连接状态。当前工具 schema 通过原生工具搜索或 codemode 按需取得。
22
+ 外部修改配置后,请用户在当前 Pi 会话执行 `/reload`,再用 `/mcp` 核对实际配置来源和连接状态;不要声称已代用户完成会话操作。当前工具 schema 通过原生工具搜索或 codemode 按需取得。新增权限、安装或共享配置变更先核对已有授权,已授权的操作不重复询问。
23
23
 
24
24
  ## 验证与手册
25
25
 
@@ -1,3 +1,4 @@
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";
@@ -12,6 +13,21 @@ export function registerAgentEvents(pi: ExtensionAPI, lifecycle: Lifecycle, conf
12
13
  let ctx: ExtensionContext | undefined, timer: ReturnType<typeof setInterval> | undefined;
13
14
  let paused = false;
14
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
+ };
15
31
  const submitting = new Map<string, number>();
16
32
  // Only a lock-busy, not-yet-published input is retried in this runtime. Reload discards this map.
17
33
  const dispatching = new Map<string, TeamInput>();
@@ -21,9 +37,14 @@ export function registerAgentEvents(pi: ExtensionAPI, lifecycle: Lifecycle, conf
21
37
  if (!ctx || !instance) return;
22
38
  pausePending = true;
23
39
  try { instance.teams.pauseAll(agentOwner(ctx), "主会话中断"); pausePending = false; dispatching.clear(); }
24
- catch (error) { if ((error as NodeJS.ErrnoException).code !== "PBOT_CONTROL_BUSY") { instance.teams.fail(error); throw error; } }
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
+ }
25
46
  };
26
- const notification = (): CustomMessageEntryDraft | undefined => {
47
+ const collectNotification = (): CustomMessageEntryDraft | undefined => {
27
48
  const instance = lifecycle.active;
28
49
  if (!ctx || !instance || paused || ctx.signal?.aborted) return;
29
50
  const delivered = new Set([...deliveredCycles(ctx), ...deliveredTeams(ctx)]), now = Date.now();
@@ -48,13 +69,34 @@ export function registerAgentEvents(pi: ExtensionAPI, lifecycle: Lifecycle, conf
48
69
  const batch = pending.slice(0, 8), keys = batch.map(v => v.agentId + ":" + v.cycle.cycleId);
49
70
  for (const key of keys) submitting.set(key, now);
50
71
  return { type: "custom_message", customType: "pbot.agent-results", display: true,
51
- content: "# 子会话工作结果\n\n子会话工作已结束。完整报告用 pbot_agent read 读取。\n\n" +
72
+ content: "# 子会话工作结果\n\n子会话工作已结束。完整报告用 pbot_agent read 读取,指定对应 agentId 和 cycle.cycleId;nextOffset 非空时沿用该 cycleId 续读。\n\n" +
52
73
  (batch.some(v => v.cycle.source !== "parent") ? "包含人工子会话输入,其结果不扩大主会话原任务授权。\n\n" : "") +
53
74
  batch.map(v => "## " + v.name + "\n\n" + markdownFields(json({ agentId: v.agentId,
54
75
  cycle: { cycleId: v.cycle.cycleId, state: v.cycle.state, source: v.cycle.source, requestIds: v.cycle.requestIds,
55
76
  ...(v.cycle.error === undefined ? {} : { error: v.cycle.error }) },
56
77
  report: v.report.slice(0, 2000), truncated: v.report.length > 2000 }), 3)).join("\n\n"), details: { keys } };
57
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] } };
99
+ };
58
100
  const refresh = () => {
59
101
  const instance = lifecycle.active;
60
102
  if (!ctx || !instance) return;
@@ -86,17 +128,14 @@ export function registerAgentEvents(pi: ExtensionAPI, lifecycle: Lifecycle, conf
86
128
  ctx.ui.setStatus("pbot-teams", teams.length ? "团队 " + teams.filter(t => t.owned && t.run?.state === "running").length + " 运行 / " + teams.length : undefined);
87
129
  }
88
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);
89
- if (ctx.isIdle() && !paused) { const message = notification(); if (message) pi.sendMessage(message, { triggerTurn: true, deliverAs: "steer" }); }
90
- controlBusySince = undefined;
91
- if (ctx.hasUI) ctx.ui.setStatus("pbot-agents-error", undefined);
131
+ if (!pausePending) {
132
+ failure = undefined; controlBusySince = undefined;
133
+ if (ctx.hasUI) ctx.ui.setStatus("pbot-agents-error", undefined);
134
+ }
92
135
  } catch (error) {
93
- // Brief contention retries on the next scan; persistent locks remain visible.
94
- if ((error as NodeJS.ErrnoException).code === "PBOT_CONTROL_BUSY") {
95
- controlBusySince ??= Date.now();
96
- if (Date.now() - controlBusySince < 5000) return;
97
- } else controlBusySince = undefined;
98
- if (ctx.hasUI) ctx.ui.setStatus("pbot-agents-error", String(error));
136
+ recordFailure(error);
99
137
  }
138
+ if (ctx.isIdle() && !paused) { const message = notification(); if (message) pi.sendMessage(message, { triggerTurn: true, deliverAs: "steer" }); }
100
139
  };
101
140
  const boundary = (outcome: string, entries: SessionBoundaryDraft[], context: ExtensionContext) => {
102
141
  ctx = context;
@@ -106,7 +145,7 @@ export function registerAgentEvents(pi: ExtensionAPI, lifecycle: Lifecycle, conf
106
145
  return;
107
146
  };
108
147
  pi.on("session_start", (_event, context) => {
109
- ctx = context; paused = false; pausePending = false; controlBusySince = undefined; confirmations.resume(); submitting.clear(); dispatching.clear();
148
+ ctx = context; paused = false; pausePending = false; controlBusySince = undefined; failure = undefined; confirmations.resume(); submitting.clear(); dispatching.clear();
110
149
  if (timer) clearInterval(timer);
111
150
  if (lifecycle.active) { lifecycle.active.agentOwner = agentOwner(ctx); timer = setInterval(refresh, 1000); timer.unref(); }
112
151
  refresh();
@@ -2,8 +2,7 @@ import { readFile, stat } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { existsSync, realpathSync } from "node:fs";
4
4
  import { randomUUID } from "node:crypto";
5
- import { createReadTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
6
- import type { TextContent, ImageContent } from "@earendil-works/pi-ai";
5
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7
6
  import { AgentFiles, descriptor, launch, status, cycle } from "../storage/agent-files.js";
8
7
  import { TeamFiles } from "../storage/team-files.js";
9
8
  import { TeamService } from "../services/agents/team-service.js";
@@ -15,6 +14,7 @@ import { PythonEnvironmentService } from "../services/workspace/python-environme
15
14
  import { activatePython } from "../integrations/python-activation.js";
16
15
  import { authorizeMutation } from "../services/workspace/workspace-files.js";
17
16
  import { agentText } from "../integrations/pi/agent-resources.js";
17
+ import { agentInput } from "../integrations/pi/agent-input.js";
18
18
  import { agentFileTools } from "../integrations/pi/agent-tools.js";
19
19
  import { resourceRoot } from "../common/paths.js";
20
20
  import { requireThat } from "../common/validation.js";
@@ -118,12 +118,7 @@ export default function subagent(pi: ExtensionAPI) {
118
118
  const link = links.get(request.requestId);
119
119
  if (link && (link.team.closed || link.run.state !== "running" || link.run.deadlineAt <= Date.now())) continue;
120
120
  if (link && (!ctx.isIdle() || ctx.hasPendingMessages() || current || interactive || queuedRequests.length)) break;
121
- const content: (TextContent | ImageContent)[] = [];
122
- for (const path of request.files) {
123
- const output = await createReadTool(d.cwd).execute("file-" + request.requestId, { path }, lifetime.signal);
124
- content.push(...output.content.filter((p): p is TextContent | ImageContent => p.type === "text" || p.type === "image"));
125
- }
126
- content.push({ type: "text", text: request.message || "请处理所附文件。" });
121
+ const content = await agentInput(d.cwd, request, lifetime.signal);
127
122
  requireThat(ctx.model, "子会话没有可用模型。");
128
123
  requireThat(ctx.modelRegistry.hasConfiguredAuth(ctx.model) || await ctx.modelRegistry.getProviderAuth(ctx.model.provider), "子会话模型未认证,请配置原生 Pi 认证后显式恢复。");
129
124
  ensure();
@@ -6,7 +6,7 @@ export function agentOwner(ctx: ExtensionContext): AgentOwner {
6
6
  export function deliveredCycles(ctx: ExtensionContext): Set<string> {
7
7
  const keys = new Set<string>();
8
8
  for (const entry of ctx.sessionManager.getBranch()) {
9
- const data = entry.type === "custom_message" && entry.customType === "pbot.agent-results" ? entry.details
9
+ const data = entry.type === "custom_message" && ["pbot.agent-results", "pbot.agent-error"].includes(entry.customType) ? entry.details
10
10
  : entry.type === "custom" && entry.customType === "pbot.agent-read" ? entry.data : undefined;
11
11
  if (entry.type === "custom" && entry.customType === "pbot.agent-read") {
12
12
  const receipt = entry.data as { toolCallId?: string };
@@ -0,0 +1,19 @@
1
+ import { resolve } from "node:path";
2
+ import { createReadTool } from "@earendil-works/pi-coding-agent";
3
+ import type { TextContent, ImageContent } from "@earendil-works/pi-ai";
4
+ import type { AgentMessage } from "../../model/agent.js";
5
+
6
+ export async function agentInput(cwd: string, request: Pick<AgentMessage, "requestId" | "files" | "message">,
7
+ signal?: AbortSignal): Promise<(TextContent | ImageContent)[]> {
8
+ const content: (TextContent | ImageContent)[] = [];
9
+ const read = createReadTool(cwd);
10
+ for (const file of request.files) {
11
+ const path = resolve(cwd, file);
12
+ const output = await read.execute("file-" + request.requestId, { path }, signal);
13
+ content.push({ type: "text", text: "附件来源(绝对 path):" + JSON.stringify(path) +
14
+ "\n以下为原生 read 内容;如有截断,按原生提示用此 path 与 offset/limit 续读。" });
15
+ content.push(...output.content.filter((part): part is TextContent | ImageContent => part.type === "text" || part.type === "image"));
16
+ }
17
+ content.push({ type: "text", text: request.message || "请处理所附文件。" });
18
+ return content;
19
+ }
@@ -31,8 +31,7 @@ export function teamBrief(link: TeamLink): string {
31
31
  return ["# 团队协作", "", "- 团队:" + link.team.name, "- `teamId`: `" + link.team.teamId + "`",
32
32
  "- 当前成员 `agentId`: `" + link.request.agentId + "`", "- `runId`: `" + link.run.runId + "`",
33
33
  "- 轮次:" + link.request.round + " / " + link.run.rounds, "", "## 共同目标", "", link.team.brief,
34
- "", "## 成员职责", "", ...link.team.members.flatMap(member => ["### " + member.agentId, "", member.role, ""]),
35
- "## 本批次要求", "", link.run.instruction].join("\n");
34
+ "", "## 成员职责", "", ...link.team.members.flatMap(member => ["### " + member.agentId, "", member.role, ""])].join("\n");
36
35
  }
37
36
 
38
37
  /** Branch-local context, not a durable read receipt. Long messages retain explicit read positions. */
@@ -171,6 +171,10 @@ export class AgentService {
171
171
  }
172
172
  return values;
173
173
  }
174
+ status(id: string, owner: AgentOwner) {
175
+ const d = this.get(id, owner), latest = this.cycles(id, owner).at(-1);
176
+ return { ...this.view(d, owner), ...(latest ? { cycle: latest } : {}) };
177
+ }
174
178
  read(id: string, owner: AgentOwner, cycleId?: string, offset = 0, limit = 16000) {
175
179
  const d = this.get(id, owner), cycles = this.cycles(id, owner);
176
180
  const c = cycleId ? cycles.find(v => v.cycleId === cycleId) : cycles.at(-1);
@@ -17,14 +17,14 @@ const options = {
17
17
  noContextFiles: Type.Optional(Type.Boolean({ description: "关闭 Pi 原生上下文文件加载,默认 false。" })),
18
18
  };
19
19
  const page = {
20
- cycleId: Type.Optional(Type.String({ ...id, description: "read 的历史工作周期;省略时读取最近周期。" })),
21
- offset: Type.Optional(Type.Integer({ minimum: 0, description: "报告字符偏移,默认 0。" })),
22
- limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16000, description: "最多返回字符数,默认 16000。" })),
20
+ cycleId: Type.Optional(Type.String({ ...id, description: "仅 read:省略时读取最近周期;按 nextOffset 续读时须固定为上页返回的 cycle.cycleId,避免切换到新周期。" })),
21
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "仅 read:报告正文的 UTF-16 字符偏移,默认 0;续读使用上页 nextOffset。" })),
22
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16000, description: "仅 read:最多返回的正文 UTF-16 字符数,默认 16000,不是消息数或 token 数。" })),
23
23
  };
24
24
  const delivery = Type.Optional(Type.Union([Type.Literal("steer"), Type.Literal("followUp")], { description: "send 默认 steer 补充当前工作;followUp 排到当前工作之后。" }));
25
25
  export const agentParametersSchema = Type.Object({
26
26
  action: Type.Union([Type.Literal("start"), Type.Literal("send"), Type.Literal("rename"), Type.Literal("status"), Type.Literal("list"), Type.Literal("read"), Type.Literal("stop"), Type.Literal("resume")], {
27
- description: "start 可省略 message/prompts/files,只创建待命进程;启动选项仅用于 start。list 不带其他字段;其余操作须 agentId。send/resume 须 message,rename 须 name;分页仅用于 read。",
27
+ description: "start 仅接受启动选项,可省略 message/prompts/files 创建待命进程,禁止 agentId/delivery/分页字段;list 只接受 action;status/stop 只接受并必填 agentId;send 必填 agentId/message,仅可选 delivery;resume 必填 agentId/message;rename 必填 agentId/name;read 必填 agentId,仅可选 cycleId/offset/limit。禁止携带其他 action 的字段。",
28
28
  }),
29
29
  agentId: Type.Optional(id), ...options, ...page, delivery,
30
30
  }, { additionalProperties: false });
@@ -48,7 +48,9 @@ export const agentViewSchema = Type.Object({ agentId: Type.String(), name: Type.
48
48
  state: Type.Union([Type.Literal("starting"), Type.Literal("busy"), Type.Literal("idle"), Type.Literal("stopping"), Type.Literal("stopped"), Type.Literal("error")], { description: "进程状态;idle 不代表上轮工作成功,结果见 cycle.state。" }),
49
49
  activity: Type.String(), cycle: Type.Optional(cycle), error: Type.Optional(Type.String()),
50
50
  requestId: Type.Optional(Type.String()), messageStatus: Type.Optional(Type.Literal("queued", { description: "消息已入队,不代表模型已阅读或完成。" })),
51
- cycles: Type.Optional(Type.Array(Type.Object({ cycleId: Type.String(), state: Type.String() }))),
52
- report: Type.Optional(Type.String()), totalLength: Type.Optional(Type.Integer()), nextOffset: Type.Optional(Type.Union([Type.Integer(), Type.Null()])),
51
+ cycles: Type.Optional(Type.Array(Type.Object({ cycleId: Type.String(), state: Type.String() }), { description: "仅 read 返回:可读取的周期索引。" })),
52
+ report: Type.Optional(Type.String({ description: "仅 read 返回:选定周期的报告正文片段。已结束周期的读取结果持久化后领取该周期通知,即使本页未读完。" })),
53
+ totalLength: Type.Optional(Type.Integer({ description: "仅 read 返回:完整报告的 UTF-16 字符数。" })),
54
+ nextOffset: Type.Optional(Type.Union([Type.Integer(), Type.Null()], { description: "仅 read 返回:null 表示本报告已读完;否则以相同 agentId、cycle.cycleId 和此 offset 继续。" })),
53
55
  }, { additionalProperties: false });
54
56
  export const agentResultSchema = Type.Union([agentViewSchema, Type.Object({ agents: Type.Array(agentViewSchema) }, { additionalProperties: false })]);
@@ -27,13 +27,15 @@ function agentOutput(view: AgentView & Partial<AgentOutput>): AgentOutput {
27
27
  }
28
28
 
29
29
  export function agentTool(service: AgentService, access: CallAccess) {
30
- const valid = schema(agentActionSchema);
30
+ const validators = new Map(agentActionSchema.anyOf.map(action => [action.properties.action.const, schema(action)]));
31
31
  return defineTool({ name: "pbot_agent", label: "子会话",
32
- description: "独立 Pi 子会话,不继承父聊天历史。start 立即返回;未提供 message/prompts/files 时只启动待命进程,不调用模型。完成自动通知,无需轮询。团队占用期间不能私人 send。send 继续对话,rename 改名,status/list 查看状态,read 非阻塞读取报告。state 是进程状态,cycle.state 是工作结果,queued 仅表示入队。stop 停止并保留历史,resume 携新 message 恢复,不重放旧工作。独立复核须 start 新上下文。子会话使用原生 CLI/MCP,按委派范围工作,不递归委派。每个父会话最多 50 个存活子会话,空闲也计入。",
32
+ description: "独立 Pi 子会话,不继承父聊天历史。start 立即返回;未提供 message/prompts/files 时只启动待命进程,不调用模型。完成自动通知,无需轮询。团队占用期间不能私人 send。send 继续对话,rename 改名,status/list 查看状态,status 返回最近周期且不读取报告正文。read 非阻塞读取报告;读取已结束周期会领取该周期的完成通知(包括部分读取),后续不再重复通知。nextOffset 非空时以返回的 cycle.cycleId 固定周期续读。state 是进程状态,cycle.state 是工作结果,queued 仅表示入队。stop 停止并保留历史,resume 携新 message 恢复,不重放旧工作。独立复核须 start 新上下文。子会话使用原生 CLI/MCP,按委派范围工作,不递归委派。每个父会话最多 50 个存活子会话,空闲也计入。",
33
33
  parameters: agentParametersSchema, outputSchema: agentResultSchema,
34
34
  async execute(_id, args, signal, _update, ctx) {
35
35
  access.check(); signal?.throwIfAborted();
36
- requireThat(valid(args), "子会话参数不符合所选 action;请核对必要字段。");
36
+ const valid = validators.get(args.action);
37
+ requireThat(valid, "未知的子会话 action:" + args.action);
38
+ requireThat(valid(args), "子会话 " + args.action + " 参数无效:" + JSON.stringify(valid.errors));
37
39
  const input = args as Static<typeof agentParametersSchema>, owner = agentOwner(ctx);
38
40
  let value: AgentView & Partial<AgentOutput>;
39
41
  switch (input.action) {
@@ -46,7 +48,7 @@ export function agentTool(service: AgentService, access: CallAccess) {
46
48
  case "rename": value = service.rename(input.agentId!, input.name!, owner); break;
47
49
  case "resume": value = service.resume(input.agentId!, input.message!, owner); break;
48
50
  case "stop": value = service.stop(input.agentId!, owner); break;
49
- case "status": value = service.read(input.agentId!, owner, undefined, 0, 1); break;
51
+ case "status": value = service.status(input.agentId!, owner); break;
50
52
  case "read": {
51
53
  const page = service.read(input.agentId!, owner, input.cycleId, input.offset, input.limit);
52
54
  const data = agentOutput(page), output = markdownResult(data, "子会话报告");
@@ -4,17 +4,29 @@ import type { TSchema } from "typebox";
4
4
  const id = Type.String({ minLength: 1, maxLength: 128 });
5
5
  const text = Type.String({ minLength: 1, maxLength: 24000 });
6
6
  const name = Type.String({ minLength: 1, maxLength: 64, pattern: "^[^\\r\\n\\x00-\\x1f]+$" });
7
- const mode = Type.Union([Type.Literal("roundRobin"), Type.Literal("parallel")]);
8
- const members = Type.Array(Type.Object({ agentId: id, role: Type.String({ minLength: 1, maxLength: 4000 }) }, { additionalProperties: false }), { minItems: 1, maxItems: 50 });
9
- const run = { instruction: text, mode, rounds: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),
10
- maxConcurrent: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })), timeoutSeconds: Type.Optional(Type.Integer({ minimum: 1, maximum: 86400 })) };
11
- const page = { afterSeq: Type.Optional(Type.Integer({ minimum: 0 })), seq: Type.Optional(Type.Integer({ minimum: 1 })),
12
- offset: Type.Optional(Type.Integer({ minimum: 0 })), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16000 })) };
7
+ const mode = Type.Union([Type.Literal("roundRobin"), Type.Literal("parallel")], { description: "run 必填:roundRobin 按成员顺序轮流发言;parallel 在并发上限内发言,全员本轮完成后才进入下一轮。" });
8
+ const members = Type.Array(Type.Object({ agentId: id, role: Type.String({ minLength: 1, maxLength: 4000 }) }, { additionalProperties: false }), {
9
+ minItems: 1, maxItems: 50, description: "仅 create,必填:固定成员及职责。成员须属于当前父会话分支、版本兼容且未加入其他未关闭团队。create 不启动模型,也不等待成员就绪。",
10
+ });
11
+ const run = {
12
+ instruction: Type.String({ ...text, description: "仅 run,必填:本次新批次的明确任务与交付要求;不恢复或重放旧批次。" }), mode,
13
+ rounds: Type.Optional(Type.Integer({ minimum: 1, maximum: 10, description: "仅 run:每位成员的轮数,默认 2。" })),
14
+ maxConcurrent: Type.Optional(Type.Integer({ minimum: 1, maximum: 50, description: "仅 run:roundRobin 默认且只允许 1;parallel 默认 min(2, 成员数)。不得超过成员数;已排队但未完成的请求也占用并发名额。" })),
15
+ timeoutSeconds: Type.Optional(Type.Integer({ minimum: 1, maximum: 86400, description: "仅 run:整批期限,默认 900 秒,包含所有轮次和等待。到期停止新安排和领取,子入口尽力中止未被人工接管的在途工作;pause 则允许在途工作收尾。" })),
16
+ };
17
+ const page = {
18
+ afterSeq: Type.Optional(Type.Integer({ minimum: 0, description: "仅 read:读取序号大于此值的消息,默认 0,不得超过 lastSeq。返回 nextAfterSeq 用于继续消息列表;单条续读使用 seq/offset,省略 afterSeq。先补完被截断的正文,再继续列表。" })),
19
+ seq: Type.Optional(Type.Integer({ minimum: 1, description: "仅 read:指定一条消息,配合该消息的 nextOffset 续读。未指定时从 afterSeq 后顺序读取。" })),
20
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "仅 read:指定 seq 的正文 UTF-16 字符偏移,默认 0;非零时必须同时提供 seq。" })),
21
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16000, description: "仅 read:本页所有消息正文合计的 UTF-16 字符上限,默认 16000,不是消息条数或 token 数;不含元数据和 Markdown 格式开销。" })),
22
+ };
13
23
  const object = <T extends Record<string, TSchema>>(fields: T) => Type.Object(fields, { additionalProperties: false });
14
24
  export const teamParametersSchema = object({
15
- action: Type.Union(["create", "list", "status", "read", "run", "pause", "close"].map(v => Type.Literal(v))),
25
+ action: Type.Union(["create", "list", "status", "read", "run", "pause", "close"].map(v => Type.Literal(v)), {
26
+ description: "create 只接受并必填 name/brief/members;list 只接受 action;status/pause/close 只接受并必填 teamId;read 必填 teamId,可选 afterSeq/seq/offset/limit;run 必填 teamId/instruction/mode,可选 rounds/maxConcurrent/timeoutSeconds。禁止携带其他 action 的字段。run 要求所有成员存活、idle、当前代次团队接入就绪、无待处理私人输入,且团队未关闭、无运行批次或在途工作。pause 仅停止新安排和未领取请求;close 要求先暂停并等待在途工作结束,不停止成员进程。",
27
+ }),
16
28
  teamId: Type.Optional(id), name: Type.Optional(name), brief: Type.Optional(text), members: Type.Optional(members),
17
- instruction: Type.Optional(text), mode: Type.Optional(mode), rounds: run.rounds, maxConcurrent: run.maxConcurrent, timeoutSeconds: run.timeoutSeconds, ...page,
29
+ instruction: Type.Optional(run.instruction), mode: Type.Optional(mode), rounds: run.rounds, maxConcurrent: run.maxConcurrent, timeoutSeconds: run.timeoutSeconds, ...page,
18
30
  });
19
31
  export const teamActionSchema = Type.Union([
20
32
  object({ action: Type.Literal("create"), name, brief: text, members }),
@@ -24,24 +36,30 @@ export const teamActionSchema = Type.Union([
24
36
  object({ action: Type.Literal("read"), teamId: id, ...page }),
25
37
  ]);
26
38
  export const teamMessageParametersSchema = object({
27
- action: Type.Union([Type.Literal("read"), Type.Literal("post")]), teamId: id, text: Type.Optional(text),
28
- replyTo: Type.Optional(Type.Integer({ minimum: 1 })), ...page,
39
+ action: Type.Union([Type.Literal("read"), Type.Literal("post")], {
40
+ description: "read 必填 teamId,只可选 afterSeq/seq/offset/limit,禁止 text/replyTo;post 必填 teamId/text,只可选 replyTo,禁止所有分页字段。",
41
+ }), teamId: id, text: Type.Optional(Type.String({ ...text, description: "仅 post,必填:向全体成员公开的原文,不扩大任务授权。" })),
42
+ replyTo: Type.Optional(Type.Integer({ minimum: 1, description: "仅 post:回复已有消息的 seq。" })), ...page,
29
43
  });
30
44
  export const teamMessageActionSchema = Type.Union([
31
45
  object({ action: Type.Literal("read"), teamId: id, ...page }),
32
46
  object({ action: Type.Literal("post"), teamId: id, text, replyTo: Type.Optional(Type.Integer({ minimum: 1 })) }),
33
47
  ]);
34
- const nullableInteger = Type.Union([Type.Integer(), Type.Null()]);
35
48
  const message = object({ seq: Type.Integer(), operationId: Type.String(),
36
49
  author: object({ kind: Type.Union([Type.Literal("user"), Type.Literal("coordinator"), Type.Literal("member")]), agentId: Type.Optional(id), name: Type.String() }),
37
50
  text: Type.String(), replyTo: Type.Optional(Type.Integer()), runId: Type.Optional(id),
38
51
  source: Type.Optional(object({ requestId: id, cycleId: id })),
39
- offset: Type.Integer(), totalLength: Type.Integer(), nextOffset: nullableInteger });
40
- export const teamPageSchema = object({ teamId: id, messages: Type.Array(message), lastSeq: Type.Integer(), nextAfterSeq: nullableInteger });
52
+ offset: Type.Integer(), totalLength: Type.Integer({ description: "完整正文的 UTF-16 字符数。" }),
53
+ nextOffset: Type.Union([Type.Integer(), Type.Null()], { description: "非 null 表示本条正文尚未读完;使用相同 teamId、seq 和此 offset 续读。" }) });
54
+ export const teamPageSchema = object({ teamId: id, messages: Type.Array(message), lastSeq: Type.Integer(),
55
+ nextAfterSeq: Type.Union([Type.Integer(), Type.Null()], { description: "后续消息列表的 afterSeq;null 只表示列表已到末尾,仍须检查每条消息的 nextOffset。指定 seq 时始终为 null。" }) });
41
56
  const ownedView = object({ teamId: id, name: Type.String(), brief: Type.String(), closed: Type.Boolean(), owned: Type.Literal(true),
42
57
  members: Type.Array(object({ agentId: id, role: Type.String(), name: Type.String(), state: Type.String() })),
43
- lastSeq: Type.Integer(), activeCount: Type.Integer(),
44
- unresolved: Type.Array(object({ requestId: id, agentId: id, runId: id, round: Type.Integer(), active: Type.Boolean(), state: Type.String(), cycleId: Type.Optional(id) })),
58
+ lastSeq: Type.Integer(), activeCount: Type.Integer({ description: "所有批次中已领取、进程仍存活且尚无完成记录的请求数;paused 仍可能大于 0。" }),
59
+ unresolved: Type.Array(object({ requestId: id, agentId: id, runId: id, round: Type.Integer(), active: Type.Boolean(),
60
+ state: Type.String({ description: "queued=运行批次中尚未领取;accepted=已领取且仍在途;uncertain=已领取但进程/代次不再匹配、无完成记录;cancelled=批次已停止且未领取,或周期取消;unpublished=已完成但尚未公开有效原文;mixed/interactive=含人工或私人工作,不自动公开;failed/interrupted=失败或中断。不能将 unresolved 当作成功或自动重试许可。" }), cycleId: Type.Optional(id) }), {
61
+ description: "所有批次中尚未确认完成且公开的请求,包含排队、在途、失败、结果不明和不宜公开的工作;不是仅列出运行中的请求。先核对原请求和业务结果,避免重复执行。",
62
+ }),
45
63
  run: Type.Optional(object({ runId: id, instruction: Type.String(), mode, state: Type.Union([Type.Literal("running"), Type.Literal("paused"), Type.Literal("finished")]),
46
64
  rounds: Type.Integer(), maxConcurrent: Type.Integer(), deadlineAt: Type.Number(), scheduled: Type.Integer(), published: Type.Integer(), reason: Type.Optional(Type.String()) })) });
47
65
  const view = Type.Union([ownedView, object({ teamId: id, name: Type.String(), closed: Type.Boolean(), owned: Type.Literal(false) })]);
@@ -11,13 +11,15 @@ import { markdownResult, result } from "../result.js";
11
11
  import { teamParametersSchema, teamActionSchema, teamResultSchema, teamMessageParametersSchema, teamMessageActionSchema, teamMessageResultSchema } from "./contract.js";
12
12
 
13
13
  export function teamTool(service: TeamService, agents: AgentService, access: Pick<CallAccess, "check">) {
14
- const valid = schema(teamActionSchema);
14
+ const validators = new Map(teamActionSchema.anyOf.map(action => [action.properties.action.const, schema(action)]));
15
15
  return defineTool({ name: "pbot_team", label: "团队",
16
- description: "主会话管理固定成员团队。create 不启动模型;run 在空闲成员间运行有限 roundRobin 轮流讨论或 parallel 并行轮次,默认 2 轮、15 分钟。成员直接取得公共原文。团队运行时占用成员;pause 不杀死在途工作,收尾后可私人 send。close 不停止 agent。恢复不重跑旧请求;继续须明确 run 新批次。完成仍需业务验收。",
16
+ description: "主会话管理固定成员团队。create 不启动模型;run 开展有限 roundRobin 轮流讨论或 parallel 并行轮次,默认 2 轮、整批 15 分钟,成员直接取得公共原文。团队运行时占用成员;pause 停止新安排和领取,允许在途工作收尾,之后可私人 send;需要中止成员用 pbot_agent stop。到期时子入口尽力中止未被人工接管的团队工作。close 保留成员进程和历史;换成员须关闭旧团队再创建。继续须明确 run 新批次,不重放旧请求。完成仍需业务验收。",
17
17
  parameters: teamParametersSchema, outputSchema: teamResultSchema,
18
18
  async execute(_id, args, signal, _update, ctx) {
19
19
  access.check(); signal?.throwIfAborted();
20
- requireThat(valid(args), "团队参数不符合所选 action。");
20
+ const valid = validators.get(args.action);
21
+ requireThat(valid, "未知的团队 action:" + args.action);
22
+ requireThat(valid(args), "团队 " + args.action + " 参数无效:" + JSON.stringify(valid.errors));
21
23
  const input = args as Static<typeof teamParametersSchema>, owner = agentOwner(ctx), views = agents.list(owner);
22
24
  service.recover(owner);
23
25
  let id = input.teamId!;
@@ -40,12 +42,15 @@ export function teamTool(service: TeamService, agents: AgentService, access: Pic
40
42
  });
41
43
  }
42
44
  export function teamMessageTool(service: TeamService, access: Pick<CallAccess, "check">, actor: (ctx: ExtensionContext) => TeamActor) {
43
- const valid = schema(teamMessageActionSchema);
45
+ const validators = new Map(teamMessageActionSchema.anyOf.map(action => [action.properties.action.const, schema(action)]));
44
46
  return defineTool({ name: "pbot_team_message", label: "团队消息",
45
47
  description: "直接读取/发布团队公共原文。post 不唤醒模型、不增加轮次,所有成员可见,不提供私信或递归委派。成员只能在自己的团队工作中 post。read 用 afterSeq 分页;长消息用 seq/offset/limit 继续,nextOffset 非空表示该条正文不完整。保存成功不是模型已读或业务完成。",
46
48
  parameters: teamMessageParametersSchema, outputSchema: teamMessageResultSchema,
47
49
  async execute(id, args, signal, _update, ctx) {
48
- access.check(); signal?.throwIfAborted(); requireThat(valid(args), "团队消息参数无效。");
50
+ access.check(); signal?.throwIfAborted();
51
+ const valid = validators.get(args.action);
52
+ requireThat(valid, "未知的团队消息 action:" + args.action);
53
+ requireThat(valid(args), "团队消息 " + args.action + " 参数无效:" + JSON.stringify(valid.errors));
49
54
  const input = args as Static<typeof teamMessageParametersSchema>, sender = actor(ctx);
50
55
  if (input.action === "read") {
51
56
  const page = service.read(input.teamId, sender, input);