personal-bot 0.2.3 → 0.2.5

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.
Files changed (234) hide show
  1. package/CONTEXT.md +13 -7
  2. package/README.md +46 -9
  3. package/README.zh-CN.md +46 -9
  4. package/bin/pbot.mjs +3 -0
  5. package/docs/README.md +4 -1
  6. package/docs/agent-collaboration.md +26 -12
  7. package/docs/architecture-guidelines.md +2 -2
  8. package/docs/capability-design.md +6 -4
  9. package/docs/dsh-implementation-plan.md +158 -0
  10. package/docs/dsh-support-proposal.md +314 -0
  11. package/docs/dsh.md +103 -0
  12. package/docs/file-storage.md +14 -9
  13. package/docs/history.md +29 -18
  14. package/docs/installation-design.md +226 -0
  15. package/docs/interaction-design.md +13 -0
  16. package/docs/release.md +5 -5
  17. package/docs/technical-design.md +29 -15
  18. package/docs/validation.md +53 -3
  19. package/lib/client.js +1071 -0
  20. package/lib/common/files.d.ts +2 -0
  21. package/lib/common/files.js +44 -0
  22. package/lib/common/json.d.ts +6 -0
  23. package/lib/common/json.js +16 -0
  24. package/lib/common/markdown.d.ts +5 -0
  25. package/lib/common/markdown.js +26 -0
  26. package/lib/common/package-source.js +16 -0
  27. package/lib/common/paths.d.ts +1 -0
  28. package/lib/common/paths.js +2 -0
  29. package/lib/common/validation.d.ts +3 -0
  30. package/lib/common/validation.js +14 -0
  31. package/lib/common/version.d.ts +3 -0
  32. package/lib/common/version.js +13 -0
  33. package/lib/dsh.d.ts +4 -0
  34. package/lib/dsh.js +22 -0
  35. package/lib/extension/dsh/agent-events.d.ts +24 -0
  36. package/lib/extension/dsh/agent-events.js +242 -0
  37. package/lib/extension/dsh/agent-runtime.d.ts +49 -0
  38. package/lib/extension/dsh/agent-runtime.js +348 -0
  39. package/lib/extension/dsh/command.d.ts +4 -0
  40. package/lib/extension/dsh/command.js +83 -0
  41. package/lib/extension/dsh/files.d.ts +5 -0
  42. package/lib/extension/dsh/files.js +67 -0
  43. package/lib/extension/dsh/history.d.ts +15 -0
  44. package/lib/extension/dsh/history.js +88 -0
  45. package/lib/extension/dsh/management.d.ts +21 -0
  46. package/lib/extension/dsh/management.js +217 -0
  47. package/lib/extension/dsh/workspace.d.ts +48 -0
  48. package/lib/extension/dsh/workspace.js +290 -0
  49. package/lib/extension/setup-cli.js +136 -0
  50. package/lib/extension/setup-guard.js +11 -0
  51. package/lib/integrations/agent-resources.d.ts +1 -0
  52. package/lib/integrations/agent-resources.js +18 -0
  53. package/lib/integrations/dsh/dsh-agents.d.ts +30 -0
  54. package/lib/integrations/dsh/dsh-agents.js +104 -0
  55. package/lib/integrations/dsh/history-summary.d.ts +11 -0
  56. package/lib/integrations/dsh/history-summary.js +40 -0
  57. package/lib/integrations/dsh/history.d.ts +16 -0
  58. package/lib/integrations/dsh/history.js +156 -0
  59. package/lib/integrations/dsh/input.d.ts +21 -0
  60. package/lib/integrations/dsh/input.js +62 -0
  61. package/lib/integrations/dsh/messages.d.ts +36 -0
  62. package/lib/integrations/dsh/messages.js +11 -0
  63. package/lib/integrations/dsh/notifications.d.ts +8 -0
  64. package/lib/integrations/dsh/notifications.js +55 -0
  65. package/lib/integrations/dsh/resources.d.ts +10 -0
  66. package/lib/integrations/dsh/resources.js +41 -0
  67. package/lib/integrations/dsh/schema.d.ts +7 -0
  68. package/lib/integrations/dsh/schema.js +28 -0
  69. package/lib/integrations/dsh/session.d.ts +14 -0
  70. package/lib/integrations/dsh/session.js +59 -0
  71. package/lib/integrations/history-summary.d.ts +2 -0
  72. package/lib/integrations/history-summary.js +21 -0
  73. package/lib/integrations/installation/pi-bridge.js +131 -0
  74. package/lib/integrations/installation/pi-setup.js +193 -0
  75. package/lib/integrations/installation/processes.js +155 -0
  76. package/lib/integrations/installation/workspace.js +21 -0
  77. package/lib/integrations/process.d.ts +14 -0
  78. package/lib/integrations/process.js +48 -0
  79. package/lib/integrations/python-activation.d.ts +2 -0
  80. package/lib/integrations/python-activation.js +29 -0
  81. package/lib/integrations/team-context.d.ts +18 -0
  82. package/lib/integrations/team-context.js +57 -0
  83. package/lib/model/agent.d.ts +92 -0
  84. package/lib/model/agent.js +1 -0
  85. package/lib/model/history.d.ts +43 -0
  86. package/lib/model/history.js +1 -0
  87. package/lib/model/team.d.ts +85 -0
  88. package/lib/model/team.js +1 -0
  89. package/lib/services/agents/agent-notices.d.ts +13 -0
  90. package/lib/services/agents/agent-notices.js +25 -0
  91. package/lib/services/agents/agent-run.d.ts +39 -0
  92. package/lib/services/agents/agent-run.js +138 -0
  93. package/lib/services/agents/agent-service.d.ts +166 -0
  94. package/lib/services/agents/agent-service.js +325 -0
  95. package/lib/services/agents/team-service.d.ts +168 -0
  96. package/lib/services/agents/team-service.js +343 -0
  97. package/lib/services/history/history-service.d.ts +103 -0
  98. package/lib/services/history/history-service.js +269 -0
  99. package/lib/services/installation/setup-service.js +121 -0
  100. package/lib/services/workspace/python-environment-service.d.ts +15 -0
  101. package/lib/services/workspace/python-environment-service.js +50 -0
  102. package/lib/services/workspace/workspace-files.d.ts +2 -0
  103. package/lib/services/workspace/workspace-files.js +17 -0
  104. package/lib/services/workspace/workspace-preparation.d.ts +2 -0
  105. package/lib/services/workspace/workspace-preparation.js +18 -0
  106. package/lib/storage/agent-files.d.ts +30 -0
  107. package/lib/storage/agent-files.js +166 -0
  108. package/lib/storage/atomic-json.d.ts +11 -0
  109. package/lib/storage/atomic-json.js +101 -0
  110. package/lib/storage/file-access.d.ts +2 -0
  111. package/lib/storage/file-access.js +26 -0
  112. package/lib/storage/history-files.d.ts +10 -0
  113. package/lib/storage/history-files.js +63 -0
  114. package/lib/storage/team-files.d.ts +16 -0
  115. package/lib/storage/team-files.js +111 -0
  116. package/lib/tools/agents/actions.d.ts +52 -0
  117. package/lib/tools/agents/actions.js +49 -0
  118. package/lib/tools/agents/contract.d.ts +163 -0
  119. package/lib/tools/agents/contract.js +56 -0
  120. package/lib/tools/agents/dsh-contract.d.ts +52 -0
  121. package/lib/tools/agents/dsh-contract.js +15 -0
  122. package/lib/tools/agents/dsh-tool.d.ts +5 -0
  123. package/lib/tools/agents/dsh-tool.js +38 -0
  124. package/lib/tools/history/actions.d.ts +70 -0
  125. package/lib/tools/history/actions.js +7 -0
  126. package/lib/tools/history/contract.d.ts +81 -0
  127. package/lib/tools/history/contract.js +32 -0
  128. package/lib/tools/history/dsh-tool.d.ts +4 -0
  129. package/lib/tools/history/dsh-tool.js +22 -0
  130. package/lib/tools/result.d.ts +18 -0
  131. package/lib/tools/result.js +10 -0
  132. package/lib/tools/teams/actions.d.ts +24 -0
  133. package/lib/tools/teams/actions.js +38 -0
  134. package/lib/tools/teams/contract.d.ts +232 -0
  135. package/lib/tools/teams/contract.js +67 -0
  136. package/lib/tools/teams/dsh-tool.d.ts +8 -0
  137. package/lib/tools/teams/dsh-tool.js +49 -0
  138. package/lib/ui/dsh/attachments.d.ts +5 -0
  139. package/lib/ui/dsh/attachments.js +39 -0
  140. package/lib/ui/dsh/elements.d.ts +17 -0
  141. package/lib/ui/dsh/elements.js +79 -0
  142. package/lib/ui/dsh/index.d.ts +4 -0
  143. package/lib/ui/dsh/index.js +13 -0
  144. package/lib/ui/dsh/management.d.ts +4 -0
  145. package/lib/ui/dsh/management.js +711 -0
  146. package/lib/ui/dsh/remote.d.ts +15 -0
  147. package/lib/ui/dsh/remote.js +11 -0
  148. package/lib/ui/dsh/styles.d.ts +1 -0
  149. package/lib/ui/dsh/styles.js +132 -0
  150. package/lib/ui/presentation.d.ts +3 -0
  151. package/lib/ui/presentation.js +12 -0
  152. package/lib/ui/setup.js +52 -0
  153. package/npm-shrinkwrap.json +11118 -783
  154. package/package.json +113 -9
  155. package/python/README.md +6 -6
  156. package/resources/agents/coordinator.md +1 -1
  157. package/resources/dsh/cordis.patch.yml +3 -0
  158. package/resources/installer/plugins.json +16 -0
  159. package/resources/prompts/workspace.md +1 -1
  160. package/resources/references/compute.md +1 -1
  161. package/resources/references/task-conductor.md +6 -4
  162. package/resources/skills/provider-connections/SKILL.md +10 -0
  163. package/scripts/README.md +13 -1
  164. package/src/common/package-source.ts +16 -0
  165. package/src/dsh.ts +22 -0
  166. package/src/extension/agent-events.ts +51 -46
  167. package/src/extension/agents-command.ts +48 -34
  168. package/src/extension/command.ts +42 -23
  169. package/src/extension/dsh/agent-events.ts +148 -0
  170. package/src/extension/dsh/agent-runtime.ts +251 -0
  171. package/src/extension/dsh/command.ts +78 -0
  172. package/src/extension/dsh/files.ts +61 -0
  173. package/src/extension/dsh/history.ts +69 -0
  174. package/src/extension/dsh/management.ts +108 -0
  175. package/src/extension/dsh/workspace.ts +206 -0
  176. package/src/extension/events.ts +6 -4
  177. package/src/extension/history.ts +15 -12
  178. package/src/extension/setup-cli.ts +109 -0
  179. package/src/extension/setup-guard.ts +12 -0
  180. package/src/extension/subagent-entry.ts +34 -63
  181. package/src/extension/teams-command.ts +53 -46
  182. package/src/integrations/{pi/agent-resources.ts → agent-resources.ts} +2 -2
  183. package/src/integrations/dsh/dsh-agents.ts +97 -0
  184. package/src/integrations/dsh/history-summary.ts +43 -0
  185. package/src/integrations/dsh/history.ts +76 -0
  186. package/src/integrations/dsh/input.ts +66 -0
  187. package/src/integrations/dsh/messages.ts +22 -0
  188. package/src/integrations/dsh/notifications.ts +46 -0
  189. package/src/integrations/dsh/resources.ts +44 -0
  190. package/src/integrations/dsh/schema.ts +26 -0
  191. package/src/integrations/dsh/session.ts +67 -0
  192. package/src/integrations/history-summary.ts +20 -0
  193. package/src/integrations/installation/pi-bridge.ts +107 -0
  194. package/src/integrations/installation/pi-setup.ts +162 -0
  195. package/src/integrations/installation/processes.ts +124 -0
  196. package/src/integrations/installation/workspace.ts +18 -0
  197. package/src/integrations/pi/history.ts +4 -14
  198. package/src/integrations/pi/pi-agents.ts +17 -6
  199. package/src/integrations/pi/team-context.ts +6 -58
  200. package/src/integrations/team-context.ts +60 -0
  201. package/src/model/agent.ts +2 -0
  202. package/src/model/history.ts +3 -1
  203. package/src/pbot.ts +1 -1
  204. package/src/services/agents/agent-notices.ts +28 -0
  205. package/src/services/agents/agent-run.ts +114 -0
  206. package/src/services/agents/agent-service.ts +127 -64
  207. package/src/services/history/history-service.ts +120 -59
  208. package/src/services/installation/setup-service.ts +145 -0
  209. package/src/storage/agent-files.ts +17 -4
  210. package/src/storage/history-files.ts +30 -5
  211. package/src/tools/agents/actions.ts +47 -0
  212. package/src/tools/agents/contract.ts +8 -5
  213. package/src/tools/agents/dsh-contract.ts +16 -0
  214. package/src/tools/agents/dsh-tool.ts +41 -0
  215. package/src/tools/agents/tool.ts +7 -43
  216. package/src/tools/history/actions.ts +9 -0
  217. package/src/tools/history/contract.ts +34 -0
  218. package/src/tools/history/dsh-tool.ts +22 -0
  219. package/src/tools/history/tool.ts +7 -30
  220. package/src/tools/teams/actions.ts +38 -0
  221. package/src/tools/teams/contract.ts +7 -3
  222. package/src/tools/teams/dsh-tool.ts +55 -0
  223. package/src/tools/teams/tool.ts +6 -31
  224. package/src/ui/agent-display.ts +7 -4
  225. package/src/ui/dsh/attachments.ts +42 -0
  226. package/src/ui/dsh/elements.ts +50 -0
  227. package/src/ui/dsh/index.ts +18 -0
  228. package/src/ui/dsh/management.ts +412 -0
  229. package/src/ui/dsh/remote.ts +20 -0
  230. package/src/ui/dsh/styles.ts +132 -0
  231. package/src/ui/pi/menu.ts +37 -0
  232. package/src/ui/pi/reader.ts +46 -0
  233. package/src/ui/presentation.ts +12 -0
  234. package/src/ui/setup.ts +40 -0
@@ -0,0 +1,109 @@
1
+ import { mkdir, readFile, realpath } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { fileURLToPath } from "node:url";
4
+ import { executeSetup, packageChoices, parseCatalog, parseSetupArgs } from "../services/installation/setup-service.js";
5
+ import { npmName, ordinaryNpm } from "../common/package-source.js";
6
+ import type { InstalledPackage, SetupPlan } from "../services/installation/setup-service.js";
7
+ import { PiBootstrapRequired, PiSetup, piPackageName } from "../integrations/installation/pi-setup.js";
8
+ import { inspectSystem, SetupProcesses } from "../integrations/installation/processes.js";
9
+ import { resolveWorkspace } from "../integrations/installation/workspace.js";
10
+ import { SetupUI } from "../ui/setup.js";
11
+
12
+ const root = new URL("../../", import.meta.url);
13
+ const quote = (value: string) => "'" + value.replaceAll("'", "'\\''") + "'";
14
+
15
+ export async function main(args: string[]): Promise<number> {
16
+ let options: ReturnType<typeof parseSetupArgs>;
17
+ try { options = parseSetupArgs(args); }
18
+ catch (error) { console.error(String(error)); return 2; }
19
+ const manifest = JSON.parse(await readFile(new URL("package.json", root), "utf8")) as { version: string };
20
+ if (options.action === "version") { console.log(manifest.version); return 0; }
21
+ if (options.action === "help") {
22
+ console.log("Personal Bot " + manifest.version + "\n\n用法:pbot setup [--workspace <目录>]\n" +
23
+ " npx personal-bot@latest setup\n\nsetup 安装或升级 Pi、pbot 和选中的插件,然后启动 Pi 执行 /pbot init。\n" +
24
+ "支持 Linux/macOS,需要 Node.js 22.19+、npm 和 Python 3.11+。\n--help 显示帮助;--version 显示安装器版本。");
25
+ return 0;
26
+ }
27
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
28
+ console.error("setup 需要交互终端。请在终端运行 npx personal-bot@latest setup。"); return 2;
29
+ }
30
+ const controller = new AbortController();
31
+ const cancel = () => controller.abort(new Error("安装已取消;已完成的步骤保留。"));
32
+ process.on("SIGINT", cancel); process.on("SIGTERM", cancel);
33
+ const ui = new SetupUI(controller), processes = new SetupProcesses(controller.signal);
34
+ try {
35
+ const cwd = process.cwd();
36
+ ui.text("Personal Bot " + manifest.version + " — 安装与升级\n正在检查 Node/npm、Python 和 tmux…");
37
+ const system = await inspectSystem(processes, cwd);
38
+ const supplied = options.workspace ?? await ui.ask("工作区目录(当前、已有或新建目录)", cwd);
39
+ const workspace = await resolveWorkspace(supplied, cwd, homedir());
40
+ const host = new PiSetup(processes, system.npm, cwd);
41
+ let pi = await host.inspectPi();
42
+ let installed: InstalledPackage[];
43
+ try { installed = await host.inspectPackages(pi, workspace); }
44
+ catch (error) {
45
+ if (!(error instanceof PiBootstrapRequired)) throw error;
46
+ const version = await host.latest(piPackageName);
47
+ ui.text(error.message + "\n需先安装 Pi 才能使用其原生接口核对这些来源。目标版本:" + version);
48
+ if (!await ui.confirm("先通过 npm 安装 Pi,再继续选择工作区插件")) return 130;
49
+ pi = await host.ensurePi(undefined, version);
50
+ installed = await host.inspectPackages(pi, workspace);
51
+ }
52
+ const catalog = parseCatalog(JSON.parse(await readFile(new URL("resources/installer/plugins.json", root), "utf8")));
53
+ const choices = packageChoices(catalog, installed), optional = choices.filter(choice => !choice.required);
54
+ ui.text("\nPersonal Bot 必装。选择配套插件(取消勾选不会卸载已有插件):");
55
+ const selected = await ui.selectMany(optional.map(choice => choice.plugin.label + " — " + choice.plugin.description),
56
+ optional.flatMap((choice, index) => choice.selected ? [index] : []));
57
+ optional.forEach((choice, index) => { choice.selected = selected.includes(index); });
58
+ for (const choice of choices.filter(item => item.selected)) {
59
+ if (choice.existing.some(pkg => !ordinaryNpm(pkg.source))) {
60
+ ui.text("\n" + choice.plugin.label + " 的已有来源:\n" + choice.existing.map(pkg =>
61
+ " " + pkg.scope + ": " + pkg.source + " (" + (pkg.version ?? "版本未知") + ")").join("\n"));
62
+ choice.keep = !await ui.confirm("切换到 npm 最新稳定版", false);
63
+ }
64
+ if (choice.keep) {
65
+ if (choice.existing.length !== 1 || !choice.existing[0]?.installedPath) choice.error = "保留来源未安装或存在多份来源,请先通过 Pi 原生配置处理。";
66
+ } else {
67
+ const others = choice.existing.filter(pkg => !npmName(pkg.source));
68
+ if (others.length > 1 || others.some(pkg => pkg.scope === "user")) {
69
+ choice.error = "请先用 pi config 调整全局或重复来源的加载范围,再切换到 npm。";
70
+ } else {
71
+ try { choice.target = await host.latest(npmName(choice.plugin.source)!); }
72
+ catch (error) { controller.signal.throwIfAborted(); choice.error = String(error); }
73
+ }
74
+ }
75
+ }
76
+ const plan: SetupPlan = { workspace, piVersion: await host.latest(piPackageName), packages: choices,
77
+ ...(pi ? { pi } : {}), ...(system.tmux ? { tmux: system.tmux } : {}) };
78
+ ui.text("\n安装计划\n工作区:" + workspace + "\nPi:" + (pi ? pi.command + " (" + pi.version + ")" : "通过 npm 全局安装") + " → " + plan.piVersion);
79
+ if (system.tmux) ui.text("tmux:" + [system.tmux.command, ...system.tmux.args].map(quote).join(" "));
80
+ for (const choice of choices) {
81
+ ui.text(" " + choice.plugin.label + ": " + (!choice.selected ? "本次跳过" : choice.error ??
82
+ (choice.keep ? "保留 " + choice.existing[0]?.source : (choice.existing.map(pkg => pkg.version ?? "未知").join(", ") || "未安装") + " → " + choice.target)));
83
+ if (choice.selected && choice.existing.some(pkg => pkg.filtered)) ui.text(" 保留已有资源过滤;如 pbot 被禁用,请通过 pi config --local 启用。");
84
+ }
85
+ const blocking = choices.find(choice => choice.required && choice.error);
86
+ if (blocking) throw new Error(blocking.error);
87
+ ui.text("升级前请停止活动 pbot 子会话和相关 Python 脚本。Pi 为当前用户共用,插件只处理此工作区。");
88
+ if (!await ui.confirm("执行以上计划")) return 130;
89
+ await mkdir(workspace, { recursive: true });
90
+ if (await realpath(workspace) !== workspace) throw new Error("工作区路径已发生变化,请重新运行 setup。");
91
+ const result = await executeSetup(plan, host, item => ui.text("[" + item.state + "] " + item.name + ": " + item.detail), controller.signal);
92
+ const failed = result.results.some(item => item.state === "failed");
93
+ if (!result.pi) return 1;
94
+ ui.text("\n软件安装阶段结束。初始化、Python 环境和外部服务的状态由 Pi/pbot/插件分别报告。");
95
+ if (await ui.confirm(failed ? "部分插件失败,是否仍先启动 Pi" : "立即启动 Pi", !failed)) {
96
+ process.stdin.pause();
97
+ await host.start(result.pi, workspace);
98
+ } else {
99
+ ui.text("稍后执行:\ncd " + quote(workspace) + "\n" + quote(result.pi.command) + " --extension " +
100
+ quote(fileURLToPath(new URL("extension/setup-guard.js", new URL("../", import.meta.url)))) + " '/pbot init'");
101
+ }
102
+ return failed ? 1 : 0;
103
+ } catch (error) {
104
+ console.error(error instanceof Error ? error.message : String(error));
105
+ return controller.signal.aborted ? 130 : 1;
106
+ } finally {
107
+ process.off("SIGINT", cancel); process.off("SIGTERM", cancel);
108
+ }
109
+ }
@@ -0,0 +1,12 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ // Native extension commands run before input handlers. Reaching this handler for
4
+ // /pbot init means it was not dispatched; never let the setup input reach a model.
5
+ export default function setupGuard(pi: ExtensionAPI) {
6
+ pi.on("input", (event, ctx) => {
7
+ if (event.text.trim() !== "/pbot init") return { action: "continue" };
8
+ ctx.ui.notify("pbot 未加载或命令存在冲突。请检查项目信任和 pi config --local 后重新运行 setup。", "error");
9
+ ctx.shutdown();
10
+ return { action: "handled" };
11
+ });
12
+ }
@@ -1,25 +1,25 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { existsSync, realpathSync } from "node:fs";
4
- import { randomUUID } from "node:crypto";
5
4
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
- import { AgentFiles, descriptor, launch, status, cycle } from "../storage/agent-files.js";
5
+ import { AgentFiles, descriptor, launch } from "../storage/agent-files.js";
6
+ import { AgentRun } from "../services/agents/agent-run.js";
7
7
  import { TeamFiles } from "../storage/team-files.js";
8
8
  import { TeamService } from "../services/agents/team-service.js";
9
9
  import { teamMessageTool } from "../tools/teams/tool.js";
10
- import { teamBrief, teamContext } from "../integrations/pi/team-context.js";
10
+ import { teamContext } from "../integrations/pi/team-context.js";
11
+ import { teamBrief } from "../integrations/team-context.js";
11
12
  import { readAgentSession } from "../integrations/pi/pi-agents.js";
12
13
  import type { TeamLink } from "../model/team.js";
13
14
  import { PythonEnvironmentService } from "../services/workspace/python-environment-service.js";
14
15
  import { activatePython } from "../integrations/python-activation.js";
15
16
  import { authorizeMutation } from "../services/workspace/workspace-files.js";
16
- import { agentText } from "../integrations/pi/agent-resources.js";
17
+ import { agentText } from "../integrations/agent-resources.js";
17
18
  import { agentInput } from "../integrations/pi/agent-input.js";
18
19
  import { agentFileTools } from "../integrations/pi/agent-tools.js";
19
20
  import { resourceRoot } from "../common/paths.js";
20
21
  import { requireThat } from "../common/validation.js";
21
22
  import { packageVersion, releaseLine } from "../common/version.js";
22
- import type { AgentCycle, AgentStatus } from "../model/agent.js";
23
23
 
24
24
  /** Explicit child composition; never installs main commands or approval capabilities. */
25
25
  export default function subagent(pi: ExtensionAPI) {
@@ -36,19 +36,10 @@ export default function subagent(pi: ExtensionAPI) {
36
36
  let ready = false, disposed = false;
37
37
  let ctx: ExtensionContext | undefined, timer: ReturnType<typeof setInterval> | undefined;
38
38
  let python: PythonEnvironmentService | undefined, restorePython: (() => void) | undefined;
39
- let processing = false, closing = false, current: AgentCycle | undefined, startLeaf: string | null = null;
40
- let queuedRequests: string[] = [], interactive = false;
41
- const accepted = new Set<string>();
42
- const saved = files.optional(id, "status.json", status);
43
- let snapshot: AgentStatus = { generation, state: "starting", activity: "启动", updatedAt: Date.now() };
44
- if (saved?.generation === generation) for (const requestId of saved.accepted ?? []) accepted.add(requestId);
39
+ let processing = false, closing = false, startLeaf: string | null = null;
40
+ const execution = new AgentRun(files, id, generation);
45
41
  let lastActivity = 0;
46
- const save = (patch: Partial<AgentStatus>) => {
47
- snapshot = { ...snapshot, ...patch, updatedAt: Date.now() };
48
- if (current) snapshot.cycle = structuredClone(current);
49
- snapshot.accepted = [...accepted];
50
- files.write(id, "status.json", snapshot, status);
51
- };
42
+ const save = execution.save.bind(execution);
52
43
  const ensure = () => {
53
44
  lifetime.signal.throwIfAborted(); files.check();
54
45
  const run = files.read(id, "launch.json", launch);
@@ -66,18 +57,16 @@ export default function subagent(pi: ExtensionAPI) {
66
57
  catch (error) { if ((error as NodeJS.ErrnoException).code !== "PBOT_CONTROL_BUSY") throw error; }
67
58
  };
68
59
  const finish = (aborted: boolean) => {
69
- if (!ctx || !current) return;
60
+ if (!ctx || !execution.current) return;
70
61
  const branch = ctx.sessionManager.getBranch();
71
62
  const at = startLeaf === null ? -1 : branch.findIndex(e => e.id === startLeaf);
72
63
  const last = branch.slice(at + 1).filter(e => e.type === "message" && e.message.role === "assistant").at(-1);
73
64
  const assistant = last?.type === "message" && last.message.role === "assistant" ? last.message : undefined;
74
65
  const text = assistant?.content.filter(p => p.type === "text").map(p => p.text).join("\n") ?? "";
75
- current = { ...current, finishedAt: Date.now(), leaf: assistant ? ctx.sessionManager.getLeafId() : null,
76
- state: aborted ? "cancelled" : assistant?.stopReason === "stop" && text.trim() ? "completed" : "failed",
77
- ...(!assistant || assistant.stopReason !== "stop" ? { error: assistant?.errorMessage ?? "没有有效最终回答。" } : {}) };
78
- files.write(id, "completions/" + current.cycleId + ".json", current, cycle, true);
79
- save({ state: "idle", activity: current.state });
80
- current = undefined; activeTeam = undefined;
66
+ execution.finish({ position: assistant ? ctx.sessionManager.getLeafId() : null, answer: text,
67
+ state: aborted ? "cancelled" : assistant?.stopReason === "stop" ? "completed" : "failed",
68
+ ...(!assistant || assistant.stopReason !== "stop" ? { error: assistant?.errorMessage ?? "没有有效最终回答。" } : {}) });
69
+ activeTeam = undefined;
81
70
  };
82
71
  const shutdown = async () => {
83
72
  if (closing) return;
@@ -105,33 +94,31 @@ export default function subagent(pi: ExtensionAPI) {
105
94
  if (activeTeam) {
106
95
  if (Date.now() >= activeTeam.run.deadlineAt) {
107
96
  pauseTeam("已到期限");
108
- if (!interactive && current?.source !== "mixed") {
97
+ if (!execution.interactive && execution.current?.source !== "mixed") {
109
98
  ctx.abort();
110
- if (!current && ctx.isIdle()) await shutdown();
99
+ if (!execution.current && ctx.isIdle()) await shutdown();
111
100
  }
112
101
  }
113
102
  return;
114
103
  }
115
104
  const links = teams.links();
116
105
  for (const request of files.messages(id)) {
117
- if (request.generation !== generation || accepted.has(request.requestId)) continue;
106
+ if (request.generation !== generation || execution.accepted(request.requestId)) continue;
118
107
  const link = links.get(request.requestId);
119
108
  if (link && (link.team.closed || link.run.state !== "running" || link.run.deadlineAt <= Date.now())) continue;
120
- if (link && (!ctx.isIdle() || ctx.hasPendingMessages() || current || interactive || queuedRequests.length)) break;
109
+ if (link && (!ctx.isIdle() || ctx.hasPendingMessages() || execution.current || execution.interactive || execution.pending)) break;
121
110
  const content = await agentInput(d.cwd, request, lifetime.signal);
122
111
  requireThat(ctx.model, "子会话没有可用模型。");
123
112
  requireThat(ctx.modelRegistry.hasConfiguredAuth(ctx.model) || await ctx.modelRegistry.getProviderAuth(ctx.model.provider), "子会话模型未认证,请配置原生 Pi 认证后显式恢复。");
124
113
  ensure();
125
114
  const accept = () => {
126
115
  bindSession();
127
- accepted.add(request.requestId);
128
116
  pi.appendEntry("pbot.agent-input", { requestId: request.requestId, generation });
129
- if (current) { current.requestIds.push(request.requestId); if (current.source === "interactive") current.source = "mixed"; }
130
- else queuedRequests.push(request.requestId);
131
- save({ state: "busy", activity: "消息已接收" });
117
+ execution.claim(request.requestId);
118
+ execution.consume(request.requestId);
132
119
  };
133
120
  if (link) {
134
- if (!ctx.isIdle() || ctx.hasPendingMessages() || current || interactive || queuedRequests.length) break;
121
+ if (!ctx.isIdle() || ctx.hasPendingMessages() || execution.current || execution.interactive || execution.pending) break;
135
122
  activeTeam = teams.claim(id, generation, request.requestId, accept);
136
123
  if (!activeTeam) continue;
137
124
  } else accept();
@@ -146,7 +133,7 @@ export default function subagent(pi: ExtensionAPI) {
146
133
  };
147
134
  pi.registerTool({ ...teamMessageTool(teams, { check: ensure }, () => ({
148
135
  kind: "member", agentId: id, generation,
149
- ...(activeTeam && current?.source === "parent" ? { requestId: activeTeam.request.requestId } : {}),
136
+ ...(activeTeam && execution.current?.source === "parent" ? { requestId: activeTeam.request.requestId } : {}),
150
137
  })), namespace: { name: "pbot", description: "团队公共通信,不提供管理或递归委派。" } });
151
138
  pi.on("resources_discover", async () => {
152
139
  if (d.options.noSkills) return {};
@@ -171,22 +158,12 @@ export default function subagent(pi: ExtensionAPI) {
171
158
  (previous === null || previous === file), "子会话身份、工作目录或文件不匹配。");
172
159
  // Native setup-only sessions have a path but no file. Do not manufacture a conversation to persist it.
173
160
  if (previous !== null || existsSync(file)) { readAgentSession(d, file, true); bindSession(); }
161
+ const receipts: { requestId: string; generation: string }[] = [];
174
162
  for (const entry of ctx.sessionManager.getEntries()) if (entry.type === "custom" && entry.customType === "pbot.agent-input") {
175
- const receipt = entry.data as { requestId?: string }; if (receipt.requestId) accepted.add(receipt.requestId);
176
- }
177
- if (saved?.generation === generation && saved.cycle?.state === "running") {
178
- const interrupted: AgentCycle = { ...saved.cycle, state: "interrupted", finishedAt: Date.now() };
179
- if (!files.completions(id).some(c => c.cycleId === interrupted.cycleId)) files.write(id, "completions/" + interrupted.cycleId + ".json", interrupted, cycle, true);
180
- }
181
- // A receipt without a cycle is uncertain, not permission to replay after reload.
182
- for (const link of teams.links().values()) {
183
- const q = link.request;
184
- if (q.agentId !== id || q.generation !== generation || !accepted.has(q.requestId) ||
185
- files.completions(id).some(c => c.generation === generation && c.requestIds.includes(q.requestId))) continue;
186
- const interrupted: AgentCycle = { cycleId: q.requestId, generation, startedAt: files.read(id, "launch.json", launch).startedAt,
187
- finishedAt: Date.now(), state: "interrupted", leaf: null, requestIds: [q.requestId], source: "parent", error: "领取后执行结果不明;未重跑。" };
188
- files.write(id, "completions/" + interrupted.cycleId + ".json", interrupted, cycle, true);
163
+ const receipt = entry.data as { requestId?: string; generation?: string };
164
+ if (typeof receipt.requestId === "string" && typeof receipt.generation === "string") receipts.push({ requestId: receipt.requestId, generation: receipt.generation });
189
165
  }
166
+ execution.recover(receipts);
190
167
  const active = pi.getActiveTools();
191
168
  for (const tool of agentFileTools(d.cwd, lifetime.signal, async (path, change, cancel) => {
192
169
  ensure();
@@ -220,7 +197,7 @@ export default function subagent(pi: ExtensionAPI) {
220
197
  });
221
198
  event.systemPromptOptions.sections.pbot_workspace = await readFile(join(resourceRoot, "prompts/workspace.md"), "utf8");
222
199
  event.systemPromptOptions.sections.pbot_child = await agentText("general-purpose", d.workspace);
223
- if (activeTeam && !interactive && current?.source !== "mixed") {
200
+ if (activeTeam && !execution.interactive && execution.current?.source !== "mixed") {
224
201
  const link = teams.links().get(activeTeam.request.requestId);
225
202
  requireThat(link, "团队关联不可读取。");
226
203
  event.systemPromptOptions.sections.pbot_team = teamBrief(link);
@@ -230,7 +207,7 @@ export default function subagent(pi: ExtensionAPI) {
230
207
  return;
231
208
  });
232
209
  pi.on("turn_end", (event, context) => {
233
- if (!activeTeam || current?.source !== "parent" || event.outcome === "aborted") return;
210
+ if (!activeTeam || execution.current?.source !== "parent" || event.outcome === "aborted") return;
234
211
  const link = teams.links().get(activeTeam.request.requestId);
235
212
  if (!link) return;
236
213
  const message = teamContext(link, context);
@@ -246,34 +223,28 @@ export default function subagent(pi: ExtensionAPI) {
246
223
  return { action: "handled" };
247
224
  }
248
225
  if (event.source !== "extension") {
249
- if (current) current.source = current.source === "parent" ? "mixed" : current.source;
250
- else interactive = true;
251
- save({ state: "busy", activity: "人工输入" });
226
+ execution.humanInput();
252
227
  pauseTeam("成员由人工接管");
253
228
  }
254
229
  return;
255
230
  });
256
231
  pi.on("agent_start", (_event, context) => {
257
232
  ctx = context;
258
- if (current) { save({ state: "busy", activity: "继续思考" }); return; }
259
- startLeaf = ctx.sessionManager.getLeafId();
260
- current = { cycleId: randomUUID(), generation, startedAt: Date.now(), state: "running", leaf: null,
261
- requestIds: queuedRequests, source: interactive ? queuedRequests.length ? "mixed" : "interactive" : "parent" };
262
- queuedRequests = []; interactive = false;
263
- save({ state: "busy", activity: "思考" });
233
+ if (!execution.current) startLeaf = ctx.sessionManager.getLeafId();
234
+ execution.begin();
264
235
  });
265
236
  pi.on("tool_call", event => {
266
237
  try {
267
238
  ensure();
268
239
  requireThat(event.toolName !== "pbot_agent" && event.toolName !== "pbot_team", "子会话禁止管理和委派。");
269
- if (activeTeam && current?.source === "parent" && Date.now() >= activeTeam.run.deadlineAt) {
240
+ if (activeTeam && execution.current?.source === "parent" && Date.now() >= activeTeam.run.deadlineAt) {
270
241
  ctx?.abort(); throw new Error("团队请求已到期限。");
271
242
  }
272
243
  save({ state: "busy", activity: event.toolName });
273
244
  } catch (error) { return { block: true, reason: String(error) }; }
274
245
  return;
275
246
  });
276
- pi.on("message_update", () => { if (current && Date.now() - lastActivity > 1000) { lastActivity = Date.now(); save({ state: "busy", activity: "回答" }); } });
247
+ pi.on("message_update", () => { if (execution.current && Date.now() - lastActivity > 1000) { lastActivity = Date.now(); save({ state: "busy", activity: "回答" }); } });
277
248
  pi.on("agent_settled", (event, context) => { ctx = context; finish(event.aborted); });
278
249
  pi.on("session_before_switch", () => ({ cancel: true }));
279
250
  pi.on("session_before_fork", () => ({ cancel: true }));
@@ -285,8 +256,8 @@ export default function subagent(pi: ExtensionAPI) {
285
256
  try {
286
257
  teams.files.setReady(id, generation, false); ready = false;
287
258
  pauseTeam("子会话关闭或重新载入");
288
- if (current) finish(true);
289
- if (event.reason !== "reload" && snapshot.state !== "error") save({ state: "stopped", activity: "已停止" });
259
+ if (execution.current) finish(true);
260
+ if (event.reason !== "reload" && execution.state !== "error") save({ state: "stopped", activity: "已停止" });
290
261
  } finally {
291
262
  try { await python?.close(); } finally { restorePython?.(); teams.dispose(); ctx = undefined; }
292
263
  }
@@ -3,58 +3,65 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-c
3
3
  import { agentOwner } from "../integrations/pi/agent-context.js";
4
4
  import type { WorkspaceInstance } from "./lifecycle.js";
5
5
  import type { Confirmations } from "../ui/confirmation.js";
6
+ import { readPage } from "../ui/pi/reader.js";
7
+ import { choose } from "../ui/pi/menu.js";
8
+ import { stateLabel } from "../ui/presentation.js";
6
9
  import { showAgents } from "./agents-command.js";
7
10
 
8
11
  export async function showTeams(pi: ExtensionAPI, instance: WorkspaceInstance, ctx: ExtensionCommandContext, confirmations: Confirmations) {
9
12
  instance.check();
10
13
  const owner = () => agentOwner(ctx), views = () => instance.agents.list(owner()), service = instance.teams;
11
14
  service.recover(owner());
12
- const values = service.list(owner(), views());
13
- if (ctx.mode !== "tui") { ctx.ui.notify(JSON.stringify({ teams: values }), "info"); return; }
14
- if (confirmations.presenting) { ctx.ui.notify("请先处理主会话批准。", "info"); return; }
15
- if (!values.length) { ctx.ui.notify("没有团队;先用 pbot_agent start 创建成员,再用 pbot_team create 组队。", "info"); return; }
16
- const options = values.map(v => v.name + (v.owned ? " · " + (v.closed ? "closed" : v.run?.state ?? "open") + " · 在途 " + v.activeCount : " · 其他分支"));
17
- const selected = await ctx.ui.select("pbot 团队", options), value = values[options.indexOf(selected ?? "")];
18
- if (!value) return;
19
- if (!value.owned) { ctx.ui.notify("请返回所属分支操作。", "info"); return; }
20
- const action = await ctx.ui.select(value.name, ["状态", "公共记录", "发布信息", "新批次", "暂停", "关闭", "成员 TUI/报告"]);
21
- instance.check();
22
- if (action === "状态") { ctx.ui.notify(JSON.stringify(service.status(value.teamId, owner(), views()), null, 2), "info"); return; }
23
- if (action === "成员 TUI/报告") { await showAgents(pi, instance, ctx, confirmations, value.members.map(m => m.agentId)); return; }
24
- if (action === "暂停") { service.pause(value.teamId, owner()); return; }
25
- if (action === "关闭") { service.close(value.teamId, owner(), views()); return; }
26
- if (action === "发布信息") {
27
- const text = await ctx.ui.editor("公开给全体成员(不会唤醒模型)");
28
- instance.check();
29
- if (text?.trim()) service.post(value.teamId, { kind: "user", owner: owner() }, randomUUID(), text);
30
- return;
31
- }
32
- if (action === "新批次") {
33
- const mode = await ctx.ui.select("协作策略", ["roundRobin", "parallel"]);
34
- if (mode !== "roundRobin" && mode !== "parallel") return;
35
- const instruction = await ctx.ui.editor("本批次要求;不要把新批次当作未知外部操作的重试");
36
- if (!instruction?.trim()) return;
37
- const rounds = await ctx.ui.input("轮数 1–10;期限默认 15 分钟", "2");
38
- if (rounds === undefined) return;
39
- instance.check();
40
- service.run(value.teamId, owner(), { instruction, mode, rounds: rounds.trim() ? Number(rounds) : 2 }, views());
41
- return;
42
- }
43
- if (action !== "公共记录") return;
44
- let page: { afterSeq?: number; seq?: number; offset?: number } = {};
15
+ if (ctx.mode !== "tui") { ctx.ui.notify(JSON.stringify({ teams: service.list(owner(), views()) }), "info"); return; }
16
+ if (confirmations.presenting) { ctx.ui.notify("请先处理待确认操作。", "info"); return; }
45
17
  for (;;) {
46
- instance.check();
47
- const data = service.read(value.teamId, { kind: "user", owner: owner() }, page);
48
- await ctx.ui.editor("公共记录(查看不领取模型通知)", data.messages.map(m =>
49
- "#" + m.seq + " " + m.author.name + (m.replyTo ? " → #" + m.replyTo : "") + "\n" + m.text +
50
- (m.nextOffset === null ? "" : "\n[本条未完,字符 " + m.nextOffset + "/" + m.totalLength + "]")).join("\n\n") || "尚无消息");
51
- const last = data.messages.at(-1);
52
- if (last?.nextOffset !== null && last?.nextOffset !== undefined) {
53
- if (await ctx.ui.select("继续阅读", ["本条后续", "返回"]) !== "本条后续") return;
54
- page = { seq: last.seq, offset: last.nextOffset };
55
- } else if (last && last.seq < data.lastSeq) {
56
- if (await ctx.ui.select("继续阅读", ["后续消息", "返回"]) !== "后续消息") return;
57
- page = { afterSeq: last.seq };
58
- } else return;
18
+ instance.check(); const values = service.list(owner(), views());
19
+ if (!values.length) { ctx.ui.notify("还没有团队。请主助手为已有专家组队。", "info"); return; }
20
+ const options = values.map(v => v.name + " · " + (v.owned ? stateLabel(v.closed ? "closed" : v.run?.state ?? "open") + " · 在途 " + v.activeCount : "其他分支"));
21
+ const selected = await choose(ctx, "团队", options), picked = values[options.indexOf(selected ?? "")]; if (!picked) return;
22
+ if (!picked.owned) { ctx.ui.notify("请返回所属分支操作。", "info"); continue; }
23
+ for (;;) {
24
+ instance.check(); const value = service.status(picked.teamId, owner(), views()); if (!value.owned) break;
25
+ const action = await choose(ctx, value.name + " · " + stateLabel(value.closed ? "closed" : value.run?.state ?? "open"),
26
+ ["状态", "公共记录", "成员会话", ...(!value.closed ? ["发布消息", ...(value.run?.state === "running" ? ["暂停"] : ["新批次"]), "关闭团队"] : [])]);
27
+ if (!action) break;
28
+ instance.check();
29
+ try {
30
+ if (action === "状态") {
31
+ const run = value.run;
32
+ await readPage(ctx, value.name, [value.brief, "", ...value.members.map(member => `${member.name} · ${member.role} · ${stateLabel(member.state)}`), "",
33
+ ...(run ? [`批次 ${stateLabel(run.state)} · ${run.mode === "parallel" ? "并行" : "轮流"} · ${run.rounds} 轮`,
34
+ `进展 已安排 ${run.scheduled} · 已交付 ${run.published} · 在途 ${value.activeCount}`, ...(run.reason ? [run.reason] : [])] : ["尚无批次"])].join("\n"));
35
+ } else if (action === "成员会话") await showAgents(pi, instance, ctx, confirmations, value.members.map(m => m.agentId));
36
+ else if (action === "暂停") { service.pause(value.teamId, owner()); ctx.ui.notify("已暂停新安排。", "info"); }
37
+ else if (action === "关闭团队") { service.close(value.teamId, owner(), views()); ctx.ui.notify("团队已关闭。", "info"); }
38
+ else if (action === "发布消息") {
39
+ const text = await ctx.ui.editor("公共消息"); instance.check();
40
+ if (text?.trim()) { service.post(value.teamId, { kind: "user", owner: owner() }, randomUUID(), text); ctx.ui.notify("消息已发布。", "info"); }
41
+ } else if (action === "新批次") {
42
+ const strategy = await ctx.ui.select("讨论模式", ["轮流讨论", "并行轮次"]); if (!strategy) continue;
43
+ const instruction = await ctx.ui.editor("任务与交付要求"); if (!instruction?.trim()) continue;
44
+ const rounds = await ctx.ui.input("轮数(1–10,默认 2)"); if (rounds === undefined) continue;
45
+ instance.check(); service.run(value.teamId, owner(), { instruction, mode: strategy === "并行轮次" ? "parallel" : "roundRobin", rounds: rounds.trim() ? Number(rounds) : 2 }, views());
46
+ ctx.ui.notify("新批次已开始。", "info");
47
+ } else if (action === "公共记录") await messages(value.teamId);
48
+ } catch (error) { ctx.ui.notify(error instanceof Error ? error.message : String(error), "error"); }
49
+ }
50
+ }
51
+ async function messages(teamId: string) {
52
+ let page: { afterSeq?: number; seq?: number; offset?: number } = {};
53
+ for (;;) {
54
+ instance.check(); const data = service.read(teamId, { kind: "user", owner: owner() }, page);
55
+ await readPage(ctx, "公共记录", data.messages.map(message => `#${message.seq} ${message.author.name}${message.replyTo ? ` → #${message.replyTo}` : ""}\n${message.text}` +
56
+ (message.nextOffset === null ? "" : `\n[未完 · ${message.nextOffset}/${message.totalLength}]`)).join("\n\n") || "尚无消息。");
57
+ const last = data.messages.at(-1);
58
+ if (last?.nextOffset !== null && last?.nextOffset !== undefined) {
59
+ if (await ctx.ui.select("公共记录", ["继续本条", "返回"]) !== "继续本条") return;
60
+ page = { seq: last.seq, offset: last.nextOffset };
61
+ } else if (last && last.seq < data.lastSeq) {
62
+ if (await ctx.ui.select("公共记录", ["后续消息", "返回"]) !== "后续消息") return;
63
+ page = { afterSeq: last.seq };
64
+ } else return;
65
+ }
59
66
  }
60
67
  }
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { resourceRoot } from "../../common/paths.js";
4
- import { requireThat } from "../../common/validation.js";
3
+ import { resourceRoot } from "../common/paths.js";
4
+ import { requireThat } from "../common/validation.js";
5
5
 
6
6
  export async function agentText(agent: "coordinator" | "general-purpose", root: string) {
7
7
  requireThat(agent === "coordinator" || agent === "general-purpose", "未知职责。");
@@ -0,0 +1,97 @@
1
+ import type { Context } from "@deepseek-ai/cordis";
2
+ import type { Agent, AgentHandle, AgentSetup } from "@deepseek-ai/dsh-agent";
3
+ import { SessionId } from "@deepseek-ai/dsh-session";
4
+ import type { AgentDescriptor, AgentSessionReference } from "../../model/agent.js";
5
+ import type { AgentHost } from "../../services/agents/agent-service.js";
6
+ import { requireThat } from "../../common/validation.js";
7
+ import { openDshAgent, readDshAgentSession, readDshReport } from "./session.js";
8
+
9
+ type NativeRun = {
10
+ generation: string;
11
+ controller: AbortController;
12
+ opening: Promise<AgentHandle>;
13
+ handle?: AgentHandle;
14
+ closing?: Promise<void>;
15
+ };
16
+
17
+ /** Native handles belong to the execution component, independently of browser observers. */
18
+ export class DshAgents implements AgentHost {
19
+ readonly kind = "dsh";
20
+ readonly #runs = new Map<string, NativeRun>();
21
+ #closed = false;
22
+ constructor(readonly ctx: Context, readonly parent: Agent,
23
+ readonly composition: {
24
+ setup(d: AgentDescriptor, directory: string, generation: string): AgentSetup;
25
+ ready(agent: Agent, generation: string): void;
26
+ stopping(agent: Agent, generation: string): Promise<void>;
27
+ }, readonly lifetime: AbortSignal) {}
28
+ available() {
29
+ this.lifetime.throwIfAborted();
30
+ requireThat(!this.#closed && this.ctx.agents.get(this.parent.id) === this.parent, "dsh 父会话或执行组件已关闭。");
31
+ }
32
+ alive(id: string) {
33
+ const run = this.#runs.get(id);
34
+ return Boolean(this.ctx.agents.get(SessionId(id))) || Boolean(run && (run.closing || !run.handle));
35
+ }
36
+ private reference(d: AgentDescriptor, reference: AgentSessionReference) {
37
+ requireThat(reference.host === "dsh" && reference.sessionId === d.agentId, "不能接管其他宿主或其他身份的原生会话。");
38
+ }
39
+ async validateSession(d: AgentDescriptor, reference: AgentSessionReference) {
40
+ this.available(); this.reference(d, reference);
41
+ requireThat(!this.alive(d.agentId), "原生专家仍被占用,不能恢复。");
42
+ await readDshAgentSession(this.ctx, d);
43
+ requireThat(!this.alive(d.agentId), "原生专家仍被占用,不能恢复。");
44
+ }
45
+ async launch(d: AgentDescriptor, directory: string, generation: string, reference: AgentSessionReference, resume: boolean, signal: AbortSignal) {
46
+ this.available(); this.reference(d, reference);
47
+ requireThat(!this.alive(d.agentId), "dsh 专家仍在运行或释放中。");
48
+ const previous = this.#runs.get(d.agentId);
49
+ if (previous) await this.release(d.agentId, previous.generation);
50
+ signal.throwIfAborted(); this.available();
51
+ const controller = new AbortController();
52
+ const combined = AbortSignal.any([signal, this.lifetime, controller.signal]);
53
+ const opening = Promise.resolve().then(() => openDshAgent(this.ctx, d, this.parent, resume,
54
+ this.composition.setup(d, directory, generation), combined));
55
+ const run: NativeRun = { generation, controller, opening };
56
+ this.#runs.set(d.agentId, run);
57
+ try {
58
+ run.handle = await opening;
59
+ if (combined.aborted) {
60
+ await this.release(d.agentId, generation);
61
+ combined.throwIfAborted();
62
+ }
63
+ this.composition.ready(run.handle.agent, generation);
64
+ } catch (error) {
65
+ if (!run.handle && this.#runs.get(d.agentId) === run) this.#runs.delete(d.agentId);
66
+ throw error;
67
+ }
68
+ }
69
+ release(id: string, generation: string) {
70
+ const run = this.#runs.get(id);
71
+ if (!run) return this.ctx.agents.get(SessionId(id)) ? Promise.reject(new Error("原生专家仍由其他执行实例持有;当前组件不能释放。")) : Promise.resolve();
72
+ if (run.generation !== generation) return Promise.resolve();
73
+ if (run.closing) return run.closing;
74
+ run.controller.abort();
75
+ run.closing = (async () => {
76
+ const handle = run.handle ?? await run.opening.catch(() => undefined);
77
+ let failure: unknown;
78
+ if (handle) {
79
+ try { await this.composition.stopping(handle.agent, generation); } catch (error) { failure = error; }
80
+ await handle.dispose();
81
+ }
82
+ if (this.#runs.get(id) === run) this.#runs.delete(id);
83
+ if (failure) throw failure;
84
+ })();
85
+ return run.closing;
86
+ }
87
+ report(d: AgentDescriptor, reference: AgentSessionReference, position: string) {
88
+ this.reference(d, reference);
89
+ return readDshReport(this.ctx, d, position);
90
+ }
91
+ async close() {
92
+ this.#closed = true;
93
+ const results = await Promise.allSettled([...this.#runs].map(([id, run]) => this.release(id, run.generation)));
94
+ const errors = results.flatMap(r => r.status === "rejected" ? [r.reason] : []);
95
+ if (errors.length) throw new AggregateError(errors, "dsh 专家仍有未释放的执行资源。");
96
+ }
97
+ }
@@ -0,0 +1,43 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type { Context } from "@deepseek-ai/cordis";
4
+ import type { Agent } from "@deepseek-ai/dsh-agent";
5
+ import type { SessionId } from "@deepseek-ai/dsh-session";
6
+ import { BlockAssembler, type PreparedLlmCall } from "@deepseek-ai/dsh-llm";
7
+ import type { HistorySummarizer } from "../../model/history.js";
8
+ import { resourceRoot } from "../../common/paths.js";
9
+ import { requireThat } from "../../common/validation.js";
10
+ import { reduceHistorySummary } from "../history-summary.js";
11
+
12
+ declare module "@deepseek-ai/dsh-llm" {
13
+ interface LlmRequestPurposeMap { "pbot-history": true }
14
+ }
15
+
16
+ /** Independent text calls through the host's routing and authentication; no Agent or Session messages. */
17
+ export function dshHistorySummarizer(ctx: Context, selection: ReturnType<Agent["model"]["current"]>, sessionId: SessionId): HistorySummarizer {
18
+ return async (text, signal) => {
19
+ requireThat(selection, "请先选择 dsh 模型。"); signal.throwIfAborted();
20
+ const system = await readFile(join(resourceRoot, "prompts/history-summary.md"), { encoding: "utf8", signal });
21
+ const prepare = () => ctx.llm.prepareCall({ provider: selection.provider, model: selection.model, maxTokens: 2048 }, signal,
22
+ (controls, model) => ({ ...controls, ...(model.reasoning?.efforts[0] ? { reasoningEffort: model.reasoning.efforts[0].id } : {}) }));
23
+ let first: PreparedLlmCall | undefined = await prepare();
24
+ const budget = Math.max(2000, Math.min(24000, Math.floor((first.context?.contextWindow ?? 12000) / 2)));
25
+ const ask: HistorySummarizer = async (content, signal) => {
26
+ signal.throwIfAborted();
27
+ const prepared = first ?? await prepare(); first = undefined;
28
+ const assembler = new BlockAssembler(); let finished = false;
29
+ for await (const chunk of prepared.stream({ ...prepared.config, system, sessionId, purpose: "pbot-history", signal, tools: [],
30
+ messages: [{ role: "user", content: [{ type: "text", text: content }] }] })) {
31
+ signal.throwIfAborted(); assembler.push(chunk); if (chunk.type === "finish") finished = true;
32
+ }
33
+ signal.throwIfAborted();
34
+ const reason = assembler.finish;
35
+ requireThat(finished && reason.kind === "stop", reason.kind === "error" || reason.kind === "aborted" ? reason.failure.message : "历史摘要未正常结束:" + reason.kind);
36
+ const blocks = assembler.blocks();
37
+ requireThat(!blocks.some(block => block.type === "tool-call"), "历史摘要不能执行工具。");
38
+ const answer = blocks.filter(block => block.type === "text").map(block => block.text).join("\n").trim();
39
+ requireThat(answer, "历史摘要为空。"); return answer;
40
+ };
41
+ return reduceHistorySummary(budget, ask)(text, signal);
42
+ };
43
+ }