personal-bot 0.2.4 → 0.2.6

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 (215) hide show
  1. package/CONTEXT.md +9 -7
  2. package/README.md +15 -8
  3. package/README.zh-CN.md +15 -8
  4. package/docs/README.md +3 -1
  5. package/docs/agent-collaboration.md +27 -13
  6. package/docs/architecture-guidelines.md +2 -2
  7. package/docs/capability-design.md +6 -4
  8. package/docs/dsh-implementation-plan.md +158 -0
  9. package/docs/dsh-support-proposal.md +314 -0
  10. package/docs/dsh.md +103 -0
  11. package/docs/file-storage.md +14 -9
  12. package/docs/history.md +29 -18
  13. package/docs/installation-design.md +3 -1
  14. package/docs/interaction-design.md +13 -0
  15. package/docs/release.md +3 -3
  16. package/docs/subagent-upgrade-proposal.md +3 -3
  17. package/docs/technical-design.md +29 -15
  18. package/docs/validation.md +49 -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/paths.d.ts +1 -0
  27. package/lib/common/paths.js +2 -0
  28. package/lib/common/validation.d.ts +3 -0
  29. package/lib/common/validation.js +14 -0
  30. package/lib/common/version.d.ts +3 -0
  31. package/lib/common/version.js +13 -0
  32. package/lib/dsh.d.ts +4 -0
  33. package/lib/dsh.js +22 -0
  34. package/lib/extension/dsh/agent-events.d.ts +24 -0
  35. package/lib/extension/dsh/agent-events.js +242 -0
  36. package/lib/extension/dsh/agent-runtime.d.ts +49 -0
  37. package/lib/extension/dsh/agent-runtime.js +348 -0
  38. package/lib/extension/dsh/command.d.ts +4 -0
  39. package/lib/extension/dsh/command.js +83 -0
  40. package/lib/extension/dsh/files.d.ts +5 -0
  41. package/lib/extension/dsh/files.js +67 -0
  42. package/lib/extension/dsh/history.d.ts +15 -0
  43. package/lib/extension/dsh/history.js +88 -0
  44. package/lib/extension/dsh/management.d.ts +21 -0
  45. package/lib/extension/dsh/management.js +217 -0
  46. package/lib/extension/dsh/workspace.d.ts +48 -0
  47. package/lib/extension/dsh/workspace.js +290 -0
  48. package/lib/integrations/agent-resources.d.ts +1 -0
  49. package/lib/integrations/agent-resources.js +18 -0
  50. package/lib/integrations/dsh/dsh-agents.d.ts +30 -0
  51. package/lib/integrations/dsh/dsh-agents.js +104 -0
  52. package/lib/integrations/dsh/history-summary.d.ts +11 -0
  53. package/lib/integrations/dsh/history-summary.js +40 -0
  54. package/lib/integrations/dsh/history.d.ts +16 -0
  55. package/lib/integrations/dsh/history.js +156 -0
  56. package/lib/integrations/dsh/input.d.ts +21 -0
  57. package/lib/integrations/dsh/input.js +62 -0
  58. package/lib/integrations/dsh/messages.d.ts +36 -0
  59. package/lib/integrations/dsh/messages.js +11 -0
  60. package/lib/integrations/dsh/notifications.d.ts +8 -0
  61. package/lib/integrations/dsh/notifications.js +55 -0
  62. package/lib/integrations/dsh/resources.d.ts +10 -0
  63. package/lib/integrations/dsh/resources.js +41 -0
  64. package/lib/integrations/dsh/schema.d.ts +7 -0
  65. package/lib/integrations/dsh/schema.js +28 -0
  66. package/lib/integrations/dsh/session.d.ts +14 -0
  67. package/lib/integrations/dsh/session.js +59 -0
  68. package/lib/integrations/history-summary.d.ts +2 -0
  69. package/lib/integrations/history-summary.js +21 -0
  70. package/lib/integrations/process.d.ts +14 -0
  71. package/lib/integrations/process.js +48 -0
  72. package/lib/integrations/python-activation.d.ts +2 -0
  73. package/lib/integrations/python-activation.js +29 -0
  74. package/lib/integrations/team-context.d.ts +18 -0
  75. package/lib/integrations/team-context.js +57 -0
  76. package/lib/model/agent.d.ts +92 -0
  77. package/lib/model/agent.js +1 -0
  78. package/lib/model/history.d.ts +43 -0
  79. package/lib/model/history.js +1 -0
  80. package/lib/model/team.d.ts +85 -0
  81. package/lib/model/team.js +1 -0
  82. package/lib/services/agents/agent-notices.d.ts +13 -0
  83. package/lib/services/agents/agent-notices.js +25 -0
  84. package/lib/services/agents/agent-run.d.ts +39 -0
  85. package/lib/services/agents/agent-run.js +138 -0
  86. package/lib/services/agents/agent-service.d.ts +166 -0
  87. package/lib/services/agents/agent-service.js +325 -0
  88. package/lib/services/agents/team-service.d.ts +168 -0
  89. package/lib/services/agents/team-service.js +343 -0
  90. package/lib/services/history/history-service.d.ts +103 -0
  91. package/lib/services/history/history-service.js +269 -0
  92. package/lib/services/workspace/python-environment-service.d.ts +15 -0
  93. package/lib/services/workspace/python-environment-service.js +50 -0
  94. package/lib/services/workspace/workspace-files.d.ts +2 -0
  95. package/lib/services/workspace/workspace-files.js +17 -0
  96. package/lib/services/workspace/workspace-preparation.d.ts +2 -0
  97. package/lib/services/workspace/workspace-preparation.js +18 -0
  98. package/lib/storage/agent-files.d.ts +30 -0
  99. package/lib/storage/agent-files.js +166 -0
  100. package/lib/storage/atomic-json.d.ts +11 -0
  101. package/lib/storage/atomic-json.js +101 -0
  102. package/lib/storage/file-access.d.ts +2 -0
  103. package/lib/storage/file-access.js +26 -0
  104. package/lib/storage/history-files.d.ts +10 -0
  105. package/lib/storage/history-files.js +63 -0
  106. package/lib/storage/team-files.d.ts +16 -0
  107. package/lib/storage/team-files.js +111 -0
  108. package/lib/tools/agents/actions.d.ts +52 -0
  109. package/lib/tools/agents/actions.js +49 -0
  110. package/lib/tools/agents/contract.d.ts +163 -0
  111. package/lib/tools/agents/contract.js +56 -0
  112. package/lib/tools/agents/dsh-contract.d.ts +52 -0
  113. package/lib/tools/agents/dsh-contract.js +15 -0
  114. package/lib/tools/agents/dsh-tool.d.ts +5 -0
  115. package/lib/tools/agents/dsh-tool.js +38 -0
  116. package/lib/tools/history/actions.d.ts +70 -0
  117. package/lib/tools/history/actions.js +7 -0
  118. package/lib/tools/history/contract.d.ts +81 -0
  119. package/lib/tools/history/contract.js +32 -0
  120. package/lib/tools/history/dsh-tool.d.ts +4 -0
  121. package/lib/tools/history/dsh-tool.js +22 -0
  122. package/lib/tools/result.d.ts +18 -0
  123. package/lib/tools/result.js +10 -0
  124. package/lib/tools/teams/actions.d.ts +24 -0
  125. package/lib/tools/teams/actions.js +38 -0
  126. package/lib/tools/teams/contract.d.ts +232 -0
  127. package/lib/tools/teams/contract.js +67 -0
  128. package/lib/tools/teams/dsh-tool.d.ts +8 -0
  129. package/lib/tools/teams/dsh-tool.js +49 -0
  130. package/lib/ui/dsh/attachments.d.ts +5 -0
  131. package/lib/ui/dsh/attachments.js +39 -0
  132. package/lib/ui/dsh/elements.d.ts +17 -0
  133. package/lib/ui/dsh/elements.js +79 -0
  134. package/lib/ui/dsh/index.d.ts +4 -0
  135. package/lib/ui/dsh/index.js +13 -0
  136. package/lib/ui/dsh/management.d.ts +4 -0
  137. package/lib/ui/dsh/management.js +711 -0
  138. package/lib/ui/dsh/remote.d.ts +15 -0
  139. package/lib/ui/dsh/remote.js +11 -0
  140. package/lib/ui/dsh/styles.d.ts +1 -0
  141. package/lib/ui/dsh/styles.js +132 -0
  142. package/lib/ui/presentation.d.ts +3 -0
  143. package/lib/ui/presentation.js +12 -0
  144. package/npm-shrinkwrap.json +11123 -793
  145. package/package.json +94 -11
  146. package/python/README.md +6 -6
  147. package/resources/agents/coordinator.md +1 -1
  148. package/resources/dsh/cordis.patch.yml +3 -0
  149. package/resources/prompts/workspace.md +1 -1
  150. package/resources/references/compute.md +1 -1
  151. package/resources/references/task-conductor.md +6 -4
  152. package/resources/skills/provider-connections/SKILL.md +10 -0
  153. package/scripts/README.md +9 -1
  154. package/src/dsh.ts +22 -0
  155. package/src/extension/agent-events.ts +51 -46
  156. package/src/extension/agents-command.ts +48 -34
  157. package/src/extension/command.ts +42 -23
  158. package/src/extension/dsh/agent-events.ts +148 -0
  159. package/src/extension/dsh/agent-runtime.ts +251 -0
  160. package/src/extension/dsh/command.ts +78 -0
  161. package/src/extension/dsh/files.ts +61 -0
  162. package/src/extension/dsh/history.ts +69 -0
  163. package/src/extension/dsh/management.ts +108 -0
  164. package/src/extension/dsh/workspace.ts +206 -0
  165. package/src/extension/events.ts +6 -4
  166. package/src/extension/history.ts +15 -12
  167. package/src/extension/subagent-entry.ts +34 -63
  168. package/src/extension/teams-command.ts +53 -46
  169. package/src/integrations/{pi/agent-resources.ts → agent-resources.ts} +2 -2
  170. package/src/integrations/dsh/dsh-agents.ts +97 -0
  171. package/src/integrations/dsh/history-summary.ts +43 -0
  172. package/src/integrations/dsh/history.ts +76 -0
  173. package/src/integrations/dsh/input.ts +66 -0
  174. package/src/integrations/dsh/messages.ts +22 -0
  175. package/src/integrations/dsh/notifications.ts +46 -0
  176. package/src/integrations/dsh/resources.ts +44 -0
  177. package/src/integrations/dsh/schema.ts +26 -0
  178. package/src/integrations/dsh/session.ts +67 -0
  179. package/src/integrations/history-summary.ts +20 -0
  180. package/src/integrations/pi/history.ts +4 -14
  181. package/src/integrations/pi/pi-agents.ts +17 -6
  182. package/src/integrations/pi/team-context.ts +6 -58
  183. package/src/integrations/team-context.ts +60 -0
  184. package/src/model/agent.ts +2 -0
  185. package/src/model/history.ts +3 -1
  186. package/src/pbot.ts +1 -1
  187. package/src/services/agents/agent-notices.ts +28 -0
  188. package/src/services/agents/agent-run.ts +114 -0
  189. package/src/services/agents/agent-service.ts +127 -64
  190. package/src/services/history/history-service.ts +120 -59
  191. package/src/storage/agent-files.ts +17 -4
  192. package/src/storage/history-files.ts +30 -5
  193. package/src/tools/agents/actions.ts +47 -0
  194. package/src/tools/agents/contract.ts +8 -5
  195. package/src/tools/agents/dsh-contract.ts +16 -0
  196. package/src/tools/agents/dsh-tool.ts +41 -0
  197. package/src/tools/agents/tool.ts +7 -43
  198. package/src/tools/history/actions.ts +9 -0
  199. package/src/tools/history/contract.ts +34 -0
  200. package/src/tools/history/dsh-tool.ts +22 -0
  201. package/src/tools/history/tool.ts +7 -30
  202. package/src/tools/teams/actions.ts +38 -0
  203. package/src/tools/teams/contract.ts +7 -3
  204. package/src/tools/teams/dsh-tool.ts +55 -0
  205. package/src/tools/teams/tool.ts +6 -31
  206. package/src/ui/agent-display.ts +7 -4
  207. package/src/ui/dsh/attachments.ts +42 -0
  208. package/src/ui/dsh/elements.ts +50 -0
  209. package/src/ui/dsh/index.ts +18 -0
  210. package/src/ui/dsh/management.ts +412 -0
  211. package/src/ui/dsh/remote.ts +20 -0
  212. package/src/ui/dsh/styles.ts +132 -0
  213. package/src/ui/pi/menu.ts +37 -0
  214. package/src/ui/pi/reader.ts +46 -0
  215. package/src/ui/presentation.ts +12 -0
@@ -0,0 +1,114 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { AgentFiles, launch, status, cycle } from "../../storage/agent-files.js";
3
+ import { requireThat } from "../../common/validation.js";
4
+ import type { AgentCycle, AgentStatus } from "../../model/agent.js";
5
+
6
+ /** Durable request receipts and work cycles, independent of a host's event vocabulary. */
7
+ export class AgentRun {
8
+ readonly #accepted = new Set<string>();
9
+ readonly #assigned = new Set<string>();
10
+ readonly #consumed = new Set<string>();
11
+ readonly #queued: string[] = [];
12
+ readonly #saved: AgentStatus | undefined;
13
+ #interactive = false;
14
+ #humanConsumed = false;
15
+ #current: AgentCycle | undefined;
16
+ #snapshot: AgentStatus;
17
+ constructor(readonly files: AgentFiles, readonly id: string, readonly generation: string) {
18
+ const saved = files.optional(id, "status.json", status);
19
+ this.#saved = saved?.generation === generation ? saved : undefined;
20
+ const requests = new Set(files.messages(id).filter(m => m.generation === generation).map(m => m.requestId));
21
+ for (const requestId of this.#saved?.accepted ?? []) if (requests.has(requestId)) this.#accepted.add(requestId);
22
+ this.#snapshot = { generation, state: "starting", activity: "启动", updatedAt: Date.now() };
23
+ }
24
+ get current(): Readonly<AgentCycle> | undefined { return this.#current; }
25
+ get state() { return this.#snapshot.state; }
26
+ get pending() { return this.#queued.length; }
27
+ get interactive() { return this.#interactive; }
28
+ accepted(requestId: string) { return this.#accepted.has(requestId); }
29
+ save(patch: Pick<AgentStatus, "state" | "activity"> & { error?: string }) {
30
+ this.#snapshot = { ...this.#snapshot, ...patch, updatedAt: Date.now(), accepted: [...this.#accepted] };
31
+ if (this.#current) this.#snapshot.cycle = structuredClone(this.#current);
32
+ this.files.write(this.id, "status.json", this.#snapshot, status);
33
+ }
34
+ /** A receipt is durable before input is passed to the native execution boundary. */
35
+ claim(requestId: string) {
36
+ requireThat(this.files.messages(this.id).some(m => m.requestId === requestId && m.generation === this.generation), "请求不属于当前启动代次。");
37
+ this.#accepted.add(requestId);
38
+ this.save({ state: "busy", activity: "消息已领取" });
39
+ }
40
+ /** Native ownership associates work even when preparation fails before model admission. */
41
+ enter(requestId: string) {
42
+ requireThat(this.#accepted.has(requestId), "请求尚未领取。");
43
+ if (this.#assigned.has(requestId)) return;
44
+ this.#assigned.add(requestId);
45
+ if (this.#current) {
46
+ if (!this.#current.requestIds.includes(requestId)) this.#current.requestIds.push(requestId);
47
+ if (this.#current.source === "interactive") this.#current.source = "mixed";
48
+ } else if (!this.#queued.includes(requestId)) this.#queued.push(requestId);
49
+ this.save({ state: "busy", activity: "消息已接收" });
50
+ }
51
+ /** Model admission is a separate fact from a native inbox claim. */
52
+ consume(requestId: string) {
53
+ requireThat(this.#accepted.has(requestId), "请求尚未领取。");
54
+ this.#consumed.add(requestId);
55
+ this.enter(requestId);
56
+ }
57
+ cancelQueued(requestId: string) {
58
+ if (!this.#accepted.has(requestId) || this.#assigned.has(requestId)) return;
59
+ const value: AgentCycle = { cycleId: requestId, generation: this.generation,
60
+ startedAt: this.files.read(this.id, "launch.json", launch).startedAt, finishedAt: Date.now(),
61
+ state: "cancelled", leaf: null, requestIds: [requestId], source: "parent" };
62
+ this.files.write(this.id, "completions/" + requestId + ".json", value, cycle, true);
63
+ this.#assigned.add(requestId);
64
+ }
65
+ humanInput() { this.#humanConsumed = true; this.enterHuman(); }
66
+ enterHuman() {
67
+ if (this.#current) {
68
+ if (this.#current.source === "parent") this.#current.source = this.#current.requestIds.length ? "mixed" : "interactive";
69
+ } else this.#interactive = true;
70
+ this.save({ state: "busy", activity: "人工输入" });
71
+ }
72
+ begin() {
73
+ if (!this.#current) {
74
+ const requestIds = this.#queued.splice(0);
75
+ this.#current = { cycleId: randomUUID(), generation: this.generation, startedAt: Date.now(), state: "running", leaf: null,
76
+ requestIds, source: this.#interactive ? requestIds.length ? "mixed" : "interactive" : "parent" };
77
+ this.#interactive = false;
78
+ }
79
+ this.save({ state: "busy", activity: "思考" });
80
+ }
81
+ finish(end: { state: Exclude<AgentCycle["state"], "running">; position: string | null; answer: string; error?: string }) {
82
+ if (!this.#current) return;
83
+ const valid = end.position !== null && end.answer.trim().length > 0 &&
84
+ (this.#current.source !== "parent" || this.#current.requestIds.length > 0) &&
85
+ this.#current.requestIds.every(id => this.#consumed.has(id)) &&
86
+ (this.#current.source === "parent" || this.#humanConsumed);
87
+ this.#current = { ...this.#current, finishedAt: Date.now(), leaf: end.position,
88
+ state: end.state === "completed" && !valid ? "failed" : end.state,
89
+ ...(end.error ? { error: end.error } : end.state === "completed" && !valid ? { error: "没有已消费的请求或有效最终回答。" } : {}) };
90
+ this.files.write(this.id, "completions/" + this.#current.cycleId + ".json", this.#current, cycle, true);
91
+ this.save({ state: "idle", activity: this.#current.state });
92
+ this.#current = undefined; this.#humanConsumed = false;
93
+ }
94
+ /** Reload seals uncertainty. It never turns a receipt into permission to replay. */
95
+ recover(receipts: Iterable<{ generation: string; requestId: string }> = []) {
96
+ const requests = new Set(this.files.messages(this.id).filter(m => m.generation === this.generation).map(m => m.requestId));
97
+ for (const receipt of receipts) if (receipt.generation === this.generation && requests.has(receipt.requestId)) this.#accepted.add(receipt.requestId);
98
+ const completed = this.files.completions(this.id);
99
+ for (const c of completed) if (c.generation === this.generation) for (const id of c.requestIds) this.#assigned.add(id);
100
+ if (this.#saved?.cycle?.state === "running" && !completed.some(c => c.cycleId === this.#saved!.cycle!.cycleId)) {
101
+ const interrupted: AgentCycle = { ...this.#saved.cycle, state: "interrupted", finishedAt: Date.now() };
102
+ this.files.write(this.id, "completions/" + interrupted.cycleId + ".json", interrupted, cycle, true);
103
+ completed.push(interrupted);
104
+ }
105
+ for (const requestId of this.#accepted) {
106
+ this.#assigned.add(requestId);
107
+ if (completed.some(c => c.generation === this.generation && c.requestIds.includes(requestId))) continue;
108
+ const interrupted: AgentCycle = { cycleId: requestId, generation: this.generation,
109
+ startedAt: this.files.read(this.id, "launch.json", launch).startedAt, finishedAt: Date.now(),
110
+ state: "interrupted", leaf: null, requestIds: [requestId], source: "parent", error: "领取后执行结果不明;未重跑。" };
111
+ this.files.write(this.id, "completions/" + requestId + ".json", interrupted, cycle, true);
112
+ }
113
+ }
114
+ }
@@ -6,23 +6,30 @@ import { requireThat } from "../../common/validation.js";
6
6
  import { AgentFiles, descriptor, launch, status, cycle } from "../../storage/agent-files.js";
7
7
  import { TeamFiles } from "../../storage/team-files.js";
8
8
  import type { TeamAgent, TeamInput } from "../../model/team.js";
9
- import type { AgentOwner, AgentOptions, AgentDescriptor, AgentCycle, AgentCompletion } from "../../model/agent.js";
9
+ import type { AgentOwner, AgentOptions, AgentDescriptor, AgentCycle, AgentCompletion, AgentSessionReference } from "../../model/agent.js";
10
10
 
11
11
  export interface AgentHost {
12
+ readonly kind: AgentSessionReference["host"];
12
13
  available(): void;
13
14
  alive(id: string): boolean;
14
- validateSession(value: AgentDescriptor, file: string): void;
15
- launch(value: AgentDescriptor, directory: string, generation: string, sessionFile: string | null): void;
16
- kill(id: string): void;
17
- report(value: AgentDescriptor, file: string, leaf: string): string;
15
+ validateSession(value: AgentDescriptor, reference: AgentSessionReference): void | Promise<void>;
16
+ launch(value: AgentDescriptor, directory: string, generation: string, reference: AgentSessionReference, resume: boolean, signal: AbortSignal): void | Promise<void>;
17
+ release(id: string, generation: string): void | Promise<void>;
18
+ report(value: AgentDescriptor, reference: AgentSessionReference, leaf: string): string | Promise<string>;
18
19
  }
19
20
  export class AgentService {
20
21
  readonly files: AgentFiles;
21
22
  readonly teamFiles: TeamFiles;
22
23
  #closed = false;
24
+ #preserve = false;
25
+ readonly #starting = new Map<string, { generation: string; controller: AbortController; stage: "validate" | "launch" }>();
26
+ readonly #launches = new Set<Promise<unknown>>();
27
+ #closing: Promise<void> | undefined;
28
+ readonly #releasing = new Map<string, Promise<void>>();
23
29
  constructor(readonly root: string, directory: string, readonly host: AgentHost) {
24
- this.files = new AgentFiles(directory); this.teamFiles = new TeamFiles(this.files);
30
+ this.files = new AgentFiles(directory, host.kind); this.teamFiles = new TeamFiles(this.files);
25
31
  }
32
+ private alive(id: string) { return this.#starting.has(id) || this.#releasing.has(id) || this.host.alive(id); }
26
33
  private check() { requireThat(!this.#closed, "子会话服务已关闭。"); }
27
34
  private owned(d: AgentDescriptor, owner: AgentOwner) {
28
35
  return d.workspace === this.root && d.parentSessionId === owner.sessionId && (d.parentLeaf === null || owner.branch.includes(d.parentLeaf));
@@ -42,18 +49,19 @@ export class AgentService {
42
49
  const run = this.files.read(d.agentId, "launch.json", launch);
43
50
  const saved = this.files.optional(d.agentId, "status.json", status);
44
51
  const current = saved?.generation === run.generation ? saved : undefined;
45
- const alive = this.host.alive(d.agentId);
52
+ const alive = this.alive(d.agentId), pending = this.#starting.get(d.agentId);
53
+ const starting = pending && !pending.controller.signal.aborted;
46
54
  return { ...d, name: run.name, generation: run.generation, owned: this.owned(d, owner), alive,
47
- state: alive ? run.stop ? "stopping" : current?.state ?? "starting" : current?.state === "error" ? "error" : "stopped",
48
- activity: alive ? current?.activity ?? "启动" : current?.error ?? "已停止",
55
+ state: alive ? starting ? "starting" : run.stop || pending?.controller.signal.aborted ? "stopping" : current?.state ?? "starting" : current?.state === "error" ? "error" : "stopped",
56
+ activity: starting ? "启动" : alive ? current?.activity ?? "启动" : current?.error ?? "已停止",
49
57
  ...(current?.cycle ? { cycle: current.cycle } : {}), ...(current?.error ? { error: current.error } : {}) };
50
58
  }
51
- start(options: AgentOptions, owner: AgentOwner) {
59
+ async start(options: AgentOptions, owner: AgentOwner) {
52
60
  this.check(); this.host.available();
53
61
  options = Object.fromEntries(Object.entries(options).filter(([, v]) => v !== undefined));
54
- requireThat(options.provider && options.model, "请选择可在子进程使用的模型。");
55
- return this.files.control(() => {
56
- requireThat(this.list(owner).filter(v => this.host.alive(v.agentId)).length < 50, "最多 50 个存活子会话;请先停止一个空闲会话。");
62
+ requireThat(options.provider && options.model, "请选择可在专家会话使用的模型。");
63
+ const prepared = this.files.control(() => {
64
+ requireThat(this.list(owner).filter(v => v.alive).length < 50, "最多 50 个存活或启动中子会话;请先停止一个空闲会话。");
57
65
  const agentId = randomUUID();
58
66
  const cwd = realpathSync(resolve(this.root, options.cwd ?? "."));
59
67
  const files = (options.files ?? []).map(p => realpathSync(resolve(this.root, p)));
@@ -63,18 +71,38 @@ export class AgentService {
63
71
  this.files.create(d, run);
64
72
  const initial = [options.message, ...(options.prompts ?? [])].filter(Boolean).join("\n\n");
65
73
  const request = initial || files.length ? this.files.enqueue(agentId, initial, "steer", files) : undefined;
66
- try { this.host.launch(d, this.files.directory(agentId), run.generation, this.files.session(agentId)); }
67
- catch (error) {
68
- this.files.write(agentId, "status.json", { generation: run.generation, state: "error", activity: "启动失败", updatedAt: Date.now(), error: String(error) }, status);
69
- throw error;
70
- }
71
- return { ...this.view(d, owner), ...(request ? { requestId: request.requestId, messageStatus: "queued" as const } : {}) };
74
+ this.#starting.set(agentId, { generation: run.generation, controller: new AbortController(), stage: "launch" });
75
+ return { d, run, request };
72
76
  });
77
+ await this.launch(prepared.d, prepared.run.generation);
78
+ return { ...this.view(prepared.d, owner), ...(prepared.request ? { requestId: prepared.request.requestId, messageStatus: "queued" as const } : {}) };
79
+ }
80
+ private async launch(d: AgentDescriptor, generation: string, resume = false) {
81
+ const pending = this.#starting.get(d.agentId);
82
+ requireThat(pending?.generation === generation, "启动预约已失效。");
83
+ const operation = (async () => {
84
+ try {
85
+ await this.host.launch(d, this.files.directory(d.agentId), generation, this.files.reference(d.agentId), resume, pending.controller.signal);
86
+ requireThat(!this.#closed || this.#preserve, "子会话服务已关闭。"); pending.controller.signal.throwIfAborted();
87
+ const run = this.files.read(d.agentId, "launch.json", launch);
88
+ requireThat(run.generation === generation && !run.stop, "子会话启动已取消或代次变化。");
89
+ } catch (error) {
90
+ const run = this.files.read(d.agentId, "launch.json", launch);
91
+ if (run.generation === generation) {
92
+ if (this.host.alive(d.agentId)) await this.release(d.agentId);
93
+ this.files.write(d.agentId, "status.json", { generation, state: run.stop ? "stopped" : "error",
94
+ activity: run.stop ? "已停止" : "启动失败", updatedAt: Date.now(), ...(run.stop ? {} : { error: String(error) }) }, status);
95
+ }
96
+ throw error;
97
+ } finally { if (this.#starting.get(d.agentId) === pending) this.#starting.delete(d.agentId); }
98
+ })();
99
+ this.#launches.add(operation);
100
+ try { await operation; } finally { this.#launches.delete(operation); }
73
101
  }
74
102
  send(id: string, text: string, delivery: "steer" | "followUp", owner: AgentOwner) {
75
103
  const d = this.get(id, owner);
76
104
  return this.files.control(() => {
77
- requireThat(this.host.alive(id) && !this.files.read(id, "launch.json", launch).stop, "子会话已停止,请显式 resume。");
105
+ requireThat(this.alive(id) && this.#starting.get(id)?.stage !== "validate" && !this.files.read(id, "launch.json", launch).stop, "子会话已停止或仍在恢复校验,请等待显式恢复完成。");
78
106
  this.assertPrivate(id);
79
107
  const request = this.files.enqueue(id, text, delivery);
80
108
  return { ...this.view(d, owner), requestId: request.requestId, messageStatus: "queued" as const };
@@ -118,10 +146,11 @@ export class AgentService {
118
146
  }
119
147
  private requestStop(id: string) {
120
148
  this.files.control(() => { const run = this.files.read(id, "launch.json", launch); run.stop = true; this.files.write(id, "launch.json", run, launch); });
149
+ this.#starting.get(id)?.controller.abort();
121
150
  }
122
- enter(id: string, owner: AgentOwner) {
151
+ async enter(id: string, owner: AgentOwner) {
123
152
  const d = this.get(id, owner);
124
- if (this.host.alive(id)) {
153
+ if (this.alive(id)) {
125
154
  requireThat(!this.files.read(id, "launch.json", launch).stop, "子会话正在停止,请稍后进入。");
126
155
  return this.view(d, owner);
127
156
  }
@@ -130,28 +159,45 @@ export class AgentService {
130
159
  resume(id: string, text: string, owner: AgentOwner) {
131
160
  return this.restart(id, owner, text);
132
161
  }
133
- private restart(id: string, owner: AgentOwner, text?: string) {
162
+ private async restart(id: string, owner: AgentOwner, text?: string) {
134
163
  const d = this.get(id, owner); this.host.available();
135
- return this.files.control(() => {
136
- requireThat(!this.host.alive(id), "子会话仍在运行。");
164
+ const reserved = this.files.control(() => {
165
+ requireThat(!this.alive(id), "子会话仍在运行。");
137
166
  this.assertPrivate(id);
138
- requireThat(this.list(owner).filter(v => this.host.alive(v.agentId)).length < 50, "最多 50 个存活子会话。");
139
- const sessionFile = this.files.session(id);
140
- if (sessionFile !== null) this.host.validateSession(d, sessionFile);
141
- const previous = this.files.read(id, "launch.json", launch);
142
- // The old process is gone: seal its interruption before replacing its generation.
143
- for (const ended of this.cycles(id, owner)) {
144
- if (ended.generation === previous.generation && !this.files.completions(id).some(c => c.cycleId === ended.cycleId)) {
145
- this.files.write(id, "completions/" + ended.cycleId + ".json", ended, cycle, true);
146
- }
147
- }
148
- this.#stopping.delete(id);
149
- const run = { ...previous, generation: randomUUID(), sequence: 0, stop: false, startedAt: Date.now() };
150
- this.files.write(id, "launch.json", run, launch);
151
- const request = text === undefined ? undefined : this.files.enqueue(id, text, "steer");
152
- this.host.launch(d, this.files.directory(id), run.generation, sessionFile);
153
- return { ...this.view(d, owner), ...(request ? { requestId: request.requestId, messageStatus: "queued" as const } : {}) };
167
+ requireThat(this.list(owner).filter(v => v.alive).length < 50, "最多 50 个存活或启动中子会话。");
168
+ const previous = this.files.read(id, "launch.json", launch), interrupted = this.cycles(id, owner);
169
+ const pending = { generation: previous.generation, controller: new AbortController(), stage: "validate" as "validate" | "launch" };
170
+ this.#starting.set(id, pending);
171
+ return { previous, interrupted, pending };
154
172
  });
173
+ const operation = (async () => {
174
+ await this.host.validateSession(d, this.files.reference(id));
175
+ requireThat(!this.#closed || this.#preserve, "子会话服务已关闭。"); reserved.pending.controller.signal.throwIfAborted();
176
+ const prepared = this.files.control(() => {
177
+ requireThat(!this.host.alive(id) && this.files.read(id, "launch.json", launch).generation === reserved.previous.generation, "子会话已由其他操作恢复。");
178
+ this.assertPrivate(id);
179
+ // Only the generation known to be stopped before validation can be sealed.
180
+ for (const ended of reserved.interrupted) {
181
+ if (ended.generation === reserved.previous.generation && !this.files.completions(id).some(c => c.cycleId === ended.cycleId)) {
182
+ this.files.write(id, "completions/" + ended.cycleId + ".json", ended, cycle, true);
183
+ }
184
+ }
185
+ this.#stopping.delete(id);
186
+ const run = { ...reserved.previous, generation: randomUUID(), sequence: 0, stop: false, startedAt: Date.now() };
187
+ this.files.write(id, "launch.json", run, launch);
188
+ const request = text === undefined ? undefined : this.files.enqueue(id, text, "steer");
189
+ reserved.pending.generation = run.generation;
190
+ reserved.pending.stage = "launch";
191
+ return { run, request };
192
+ });
193
+ await this.launch(d, prepared.run.generation, true);
194
+ return { ...this.view(d, owner), ...(prepared.request ? { requestId: prepared.request.requestId, messageStatus: "queued" as const } : {}) };
195
+ })();
196
+ this.#launches.add(operation);
197
+ try { return await operation; } finally {
198
+ this.#launches.delete(operation);
199
+ if (this.#starting.get(id) === reserved.pending) this.#starting.delete(id);
200
+ }
155
201
  }
156
202
  cycles(id: string, owner: AgentOwner): AgentCycle[] {
157
203
  const d = this.get(id, owner), view = this.view(d, owner), values = this.files.completions(id);
@@ -175,49 +221,66 @@ export class AgentService {
175
221
  const d = this.get(id, owner), latest = this.cycles(id, owner).at(-1);
176
222
  return { ...this.view(d, owner), ...(latest ? { cycle: latest } : {}) };
177
223
  }
178
- read(id: string, owner: AgentOwner, cycleId?: string, offset = 0, limit = 16000) {
224
+ async read(id: string, owner: AgentOwner, cycleId?: string, offset = 0, limit = 16000) {
179
225
  const d = this.get(id, owner), cycles = this.cycles(id, owner);
180
226
  const c = cycleId ? cycles.find(v => v.cycleId === cycleId) : cycles.at(-1);
181
227
  requireThat(!cycleId || c, "周期不存在。");
182
- const report = c ? this.report(d, c) : "";
228
+ const report = c ? await this.report(d, c) : "";
229
+ this.check();
183
230
  requireThat(offset <= report.length, "分页位置超出报告。");
184
231
  return { ...this.view(d, owner), ...(c ? { cycle: c } : {}), cycles: cycles.map(v => ({ cycleId: v.cycleId, state: v.state })),
185
232
  report: report.slice(offset, offset + limit), totalLength: report.length, nextOffset: offset + limit < report.length ? offset + limit : null };
186
233
  }
187
- completions(owner: AgentOwner): AgentCompletion[] {
188
- return this.list(owner).filter(v => v.owned).flatMap(v => this.cycles(v.agentId, owner).filter(c => c.state !== "running").map(c =>
189
- ({ agentId: v.agentId, name: v.name, cycle: c, report: this.report(v, c) })));
234
+ async completions(owner: AgentOwner): Promise<AgentCompletion[]> {
235
+ const values = await Promise.all(this.list(owner).filter(v => v.owned).flatMap(v => this.cycles(v.agentId, owner).filter(c => c.state !== "running").map(async c =>
236
+ ({ agentId: v.agentId, name: v.name, cycle: c, report: await this.report(v, c) }))));
237
+ this.check(); return values;
190
238
  }
191
239
  private report(d: AgentDescriptor, c: AgentCycle) {
192
240
  if (c.leaf === null) return "";
193
- const file = this.files.session(d.agentId);
194
- requireThat(file !== null, "完成记录缺少原生会话文件引用。");
195
- return this.host.report(d, file, c.leaf);
241
+ return this.host.report(d, this.files.reference(d.agentId), c.leaf);
242
+ }
243
+ private release(id: string) {
244
+ let pending = this.#releasing.get(id);
245
+ if (!pending) {
246
+ const generation = this.files.read(id, "launch.json", launch).generation;
247
+ pending = Promise.resolve().then(async () => {
248
+ if (this.files.read(id, "launch.json", launch).generation === generation) await this.host.release(id, generation);
249
+ }).finally(() => this.#releasing.delete(id));
250
+ this.#releasing.set(id, pending);
251
+ }
252
+ return pending;
196
253
  }
197
254
  /** Called by the parent scanner; stop remains nonblocking to the model. */
198
- refresh(owner: AgentOwner) {
255
+ async refresh(owner: AgentOwner) {
256
+ const releases: Promise<void>[] = [];
199
257
  for (const d of this.records(owner)) {
200
258
  const run = this.files.read(d.agentId, "launch.json", launch);
201
- // A stopped request gets its own grace deadline without modifying child status.
202
259
  if (!run.stop) this.#stopping.delete(d.agentId);
203
- if (run.stop && this.host.alive(d.agentId)) {
260
+ if (run.stop && this.alive(d.agentId)) {
261
+ this.#starting.get(d.agentId)?.controller.abort();
204
262
  const since = this.#stopping.get(d.agentId) ?? Date.now(); this.#stopping.set(d.agentId, since);
205
- if (Date.now() - since > 5000) this.host.kill(d.agentId);
263
+ if (Date.now() - since > 5000) releases.push(this.release(d.agentId));
206
264
  }
207
265
  }
266
+ await Promise.all(releases);
208
267
  }
209
268
  readonly #stopping = new Map<string, number>();
210
- async close(owner?: AgentOwner, preserve = false) {
269
+ close(owner?: AgentOwner, preserve = false) {
270
+ return this.#closing ??= this.dispose(owner, preserve);
271
+ }
272
+ private async dispose(owner?: AgentOwner, preserve = false) {
211
273
  if (this.#closed) return;
212
- if (!preserve && owner) {
213
- const ids = this.list(owner).filter(v => this.host.alive(v.agentId)).map(v => v.agentId);
214
- const errors: unknown[] = [];
215
- for (const id of ids) { try { this.requestStop(id); } catch (error) { errors.push(error); } }
216
- const deadline = Date.now() + 5000;
217
- while (ids.some(id => this.host.alive(id)) && Date.now() < deadline) await new Promise(r => setTimeout(r, 100));
218
- for (const id of ids) if (this.host.alive(id)) this.host.kill(id);
219
- if (errors.length) { this.#closed = true; throw new AggregateError(errors, "停止记录未能保存;已清理子进程。"); }
220
- }
221
- this.#closed = true;
274
+ const ids = !preserve && owner ? this.list(owner).filter(v => v.alive).map(v => v.agentId) : [];
275
+ this.#closed = true; this.#preserve = preserve;
276
+ const errors: unknown[] = [];
277
+ for (const id of ids) { try { this.requestStop(id); } catch (error) { errors.push(error); } }
278
+ if (!preserve) for (const pending of this.#starting.values()) pending.controller.abort();
279
+ const deadline = Date.now() + 5000;
280
+ while (ids.some(id => this.alive(id)) && Date.now() < deadline) await new Promise(r => setTimeout(r, 100));
281
+ const released = await Promise.allSettled(ids.filter(id => this.host.alive(id)).map(id => this.release(id)));
282
+ for (const value of released) if (value.status === "rejected") errors.push(value.reason);
283
+ await Promise.allSettled([...this.#launches]);
284
+ if (errors.length) throw new AggregateError(errors, "停止或释放专家失败;请核对仍存活的执行资源。");
222
285
  }
223
286
  }