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,138 @@
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
+ /** Durable request receipts and work cycles, independent of a host's event vocabulary. */
5
+ export class AgentRun {
6
+ files;
7
+ id;
8
+ generation;
9
+ #accepted = new Set();
10
+ #assigned = new Set();
11
+ #consumed = new Set();
12
+ #queued = [];
13
+ #saved;
14
+ #interactive = false;
15
+ #humanConsumed = false;
16
+ #current;
17
+ #snapshot;
18
+ constructor(files, id, generation) {
19
+ this.files = files;
20
+ this.id = id;
21
+ this.generation = generation;
22
+ const saved = files.optional(id, "status.json", status);
23
+ this.#saved = saved?.generation === generation ? saved : undefined;
24
+ const requests = new Set(files.messages(id).filter(m => m.generation === generation).map(m => m.requestId));
25
+ for (const requestId of this.#saved?.accepted ?? [])
26
+ if (requests.has(requestId))
27
+ this.#accepted.add(requestId);
28
+ this.#snapshot = { generation, state: "starting", activity: "启动", updatedAt: Date.now() };
29
+ }
30
+ get current() { return this.#current; }
31
+ get state() { return this.#snapshot.state; }
32
+ get pending() { return this.#queued.length; }
33
+ get interactive() { return this.#interactive; }
34
+ accepted(requestId) { return this.#accepted.has(requestId); }
35
+ save(patch) {
36
+ this.#snapshot = { ...this.#snapshot, ...patch, updatedAt: Date.now(), accepted: [...this.#accepted] };
37
+ if (this.#current)
38
+ this.#snapshot.cycle = structuredClone(this.#current);
39
+ this.files.write(this.id, "status.json", this.#snapshot, status);
40
+ }
41
+ /** A receipt is durable before input is passed to the native execution boundary. */
42
+ claim(requestId) {
43
+ requireThat(this.files.messages(this.id).some(m => m.requestId === requestId && m.generation === this.generation), "请求不属于当前启动代次。");
44
+ this.#accepted.add(requestId);
45
+ this.save({ state: "busy", activity: "消息已领取" });
46
+ }
47
+ /** Native ownership associates work even when preparation fails before model admission. */
48
+ enter(requestId) {
49
+ requireThat(this.#accepted.has(requestId), "请求尚未领取。");
50
+ if (this.#assigned.has(requestId))
51
+ return;
52
+ this.#assigned.add(requestId);
53
+ if (this.#current) {
54
+ if (!this.#current.requestIds.includes(requestId))
55
+ this.#current.requestIds.push(requestId);
56
+ if (this.#current.source === "interactive")
57
+ this.#current.source = "mixed";
58
+ }
59
+ else if (!this.#queued.includes(requestId))
60
+ this.#queued.push(requestId);
61
+ this.save({ state: "busy", activity: "消息已接收" });
62
+ }
63
+ /** Model admission is a separate fact from a native inbox claim. */
64
+ consume(requestId) {
65
+ requireThat(this.#accepted.has(requestId), "请求尚未领取。");
66
+ this.#consumed.add(requestId);
67
+ this.enter(requestId);
68
+ }
69
+ cancelQueued(requestId) {
70
+ if (!this.#accepted.has(requestId) || this.#assigned.has(requestId))
71
+ return;
72
+ const value = { cycleId: requestId, generation: this.generation,
73
+ startedAt: this.files.read(this.id, "launch.json", launch).startedAt, finishedAt: Date.now(),
74
+ state: "cancelled", leaf: null, requestIds: [requestId], source: "parent" };
75
+ this.files.write(this.id, "completions/" + requestId + ".json", value, cycle, true);
76
+ this.#assigned.add(requestId);
77
+ }
78
+ humanInput() { this.#humanConsumed = true; this.enterHuman(); }
79
+ enterHuman() {
80
+ if (this.#current) {
81
+ if (this.#current.source === "parent")
82
+ this.#current.source = this.#current.requestIds.length ? "mixed" : "interactive";
83
+ }
84
+ else
85
+ this.#interactive = true;
86
+ this.save({ state: "busy", activity: "人工输入" });
87
+ }
88
+ begin() {
89
+ if (!this.#current) {
90
+ const requestIds = this.#queued.splice(0);
91
+ this.#current = { cycleId: randomUUID(), generation: this.generation, startedAt: Date.now(), state: "running", leaf: null,
92
+ requestIds, source: this.#interactive ? requestIds.length ? "mixed" : "interactive" : "parent" };
93
+ this.#interactive = false;
94
+ }
95
+ this.save({ state: "busy", activity: "思考" });
96
+ }
97
+ finish(end) {
98
+ if (!this.#current)
99
+ return;
100
+ const valid = end.position !== null && end.answer.trim().length > 0 &&
101
+ (this.#current.source !== "parent" || this.#current.requestIds.length > 0) &&
102
+ this.#current.requestIds.every(id => this.#consumed.has(id)) &&
103
+ (this.#current.source === "parent" || this.#humanConsumed);
104
+ this.#current = { ...this.#current, finishedAt: Date.now(), leaf: end.position,
105
+ state: end.state === "completed" && !valid ? "failed" : end.state,
106
+ ...(end.error ? { error: end.error } : end.state === "completed" && !valid ? { error: "没有已消费的请求或有效最终回答。" } : {}) };
107
+ this.files.write(this.id, "completions/" + this.#current.cycleId + ".json", this.#current, cycle, true);
108
+ this.save({ state: "idle", activity: this.#current.state });
109
+ this.#current = undefined;
110
+ this.#humanConsumed = false;
111
+ }
112
+ /** Reload seals uncertainty. It never turns a receipt into permission to replay. */
113
+ recover(receipts = []) {
114
+ const requests = new Set(this.files.messages(this.id).filter(m => m.generation === this.generation).map(m => m.requestId));
115
+ for (const receipt of receipts)
116
+ if (receipt.generation === this.generation && requests.has(receipt.requestId))
117
+ this.#accepted.add(receipt.requestId);
118
+ const completed = this.files.completions(this.id);
119
+ for (const c of completed)
120
+ if (c.generation === this.generation)
121
+ for (const id of c.requestIds)
122
+ this.#assigned.add(id);
123
+ if (this.#saved?.cycle?.state === "running" && !completed.some(c => c.cycleId === this.#saved.cycle.cycleId)) {
124
+ const interrupted = { ...this.#saved.cycle, state: "interrupted", finishedAt: Date.now() };
125
+ this.files.write(this.id, "completions/" + interrupted.cycleId + ".json", interrupted, cycle, true);
126
+ completed.push(interrupted);
127
+ }
128
+ for (const requestId of this.#accepted) {
129
+ this.#assigned.add(requestId);
130
+ if (completed.some(c => c.generation === this.generation && c.requestIds.includes(requestId)))
131
+ continue;
132
+ const interrupted = { cycleId: requestId, generation: this.generation,
133
+ startedAt: this.files.read(this.id, "launch.json", launch).startedAt, finishedAt: Date.now(),
134
+ state: "interrupted", leaf: null, requestIds: [requestId], source: "parent", error: "领取后执行结果不明;未重跑。" };
135
+ this.files.write(this.id, "completions/" + requestId + ".json", interrupted, cycle, true);
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,166 @@
1
+ import { AgentFiles } from "../../storage/agent-files.js";
2
+ import { TeamFiles } from "../../storage/team-files.js";
3
+ import type { TeamAgent, TeamInput } from "../../model/team.js";
4
+ import type { AgentOwner, AgentOptions, AgentDescriptor, AgentCycle, AgentCompletion, AgentSessionReference } from "../../model/agent.js";
5
+ export interface AgentHost {
6
+ readonly kind: AgentSessionReference["host"];
7
+ available(): void;
8
+ alive(id: string): boolean;
9
+ validateSession(value: AgentDescriptor, reference: AgentSessionReference): void | Promise<void>;
10
+ launch(value: AgentDescriptor, directory: string, generation: string, reference: AgentSessionReference, resume: boolean, signal: AbortSignal): void | Promise<void>;
11
+ release(id: string, generation: string): void | Promise<void>;
12
+ report(value: AgentDescriptor, reference: AgentSessionReference, leaf: string): string | Promise<string>;
13
+ }
14
+ export declare class AgentService {
15
+ #private;
16
+ readonly root: string;
17
+ readonly host: AgentHost;
18
+ readonly files: AgentFiles;
19
+ readonly teamFiles: TeamFiles;
20
+ constructor(root: string, directory: string, host: AgentHost);
21
+ private alive;
22
+ private check;
23
+ private owned;
24
+ private get;
25
+ private records;
26
+ list(owner: AgentOwner): TeamAgent[];
27
+ private view;
28
+ start(options: AgentOptions, owner: AgentOwner): Promise<{
29
+ requestId?: string;
30
+ messageStatus?: "queued";
31
+ name: string;
32
+ generation: string;
33
+ state: import("../../model/agent.js").AgentStatus["state"];
34
+ activity: string;
35
+ cycle?: AgentCycle;
36
+ error?: string;
37
+ owned: boolean;
38
+ agentId: string;
39
+ release: string;
40
+ workspace: string;
41
+ cwd: string;
42
+ parentSessionId: string;
43
+ parentLeaf: string | null;
44
+ createdAt: number;
45
+ options: AgentOptions;
46
+ alive: boolean;
47
+ }>;
48
+ private launch;
49
+ send(id: string, text: string, delivery: "steer" | "followUp", owner: AgentOwner): {
50
+ requestId: string;
51
+ messageStatus: "queued";
52
+ name: string;
53
+ generation: string;
54
+ state: import("../../model/agent.js").AgentStatus["state"];
55
+ activity: string;
56
+ cycle?: AgentCycle;
57
+ error?: string;
58
+ owned: boolean;
59
+ agentId: string;
60
+ release: string;
61
+ workspace: string;
62
+ cwd: string;
63
+ parentSessionId: string;
64
+ parentLeaf: string | null;
65
+ createdAt: number;
66
+ options: AgentOptions;
67
+ alive: boolean;
68
+ };
69
+ private assertPrivate;
70
+ enqueueTeam(input: TeamInput, owner: AgentOwner): {
71
+ requestId: string;
72
+ messageStatus: "queued";
73
+ name: string;
74
+ generation: string;
75
+ state: import("../../model/agent.js").AgentStatus["state"];
76
+ activity: string;
77
+ cycle?: AgentCycle;
78
+ error?: string;
79
+ owned: boolean;
80
+ agentId: string;
81
+ release: string;
82
+ workspace: string;
83
+ cwd: string;
84
+ parentSessionId: string;
85
+ parentLeaf: string | null;
86
+ createdAt: number;
87
+ options: AgentOptions;
88
+ alive: boolean;
89
+ };
90
+ rename(id: string, name: string, owner: AgentOwner): TeamAgent;
91
+ stop(id: string, owner: AgentOwner): TeamAgent;
92
+ private requestStop;
93
+ enter(id: string, owner: AgentOwner): Promise<TeamAgent>;
94
+ resume(id: string, text: string, owner: AgentOwner): Promise<{
95
+ requestId?: string;
96
+ messageStatus?: "queued";
97
+ name: string;
98
+ generation: string;
99
+ state: import("../../model/agent.js").AgentStatus["state"];
100
+ activity: string;
101
+ cycle?: AgentCycle;
102
+ error?: string;
103
+ owned: boolean;
104
+ agentId: string;
105
+ release: string;
106
+ workspace: string;
107
+ cwd: string;
108
+ parentSessionId: string;
109
+ parentLeaf: string | null;
110
+ createdAt: number;
111
+ options: AgentOptions;
112
+ alive: boolean;
113
+ }>;
114
+ private restart;
115
+ cycles(id: string, owner: AgentOwner): AgentCycle[];
116
+ status(id: string, owner: AgentOwner): {
117
+ cycle?: AgentCycle;
118
+ name: string;
119
+ generation: string;
120
+ state: import("../../model/agent.js").AgentStatus["state"];
121
+ activity: string;
122
+ error?: string;
123
+ owned: boolean;
124
+ agentId: string;
125
+ release: string;
126
+ workspace: string;
127
+ cwd: string;
128
+ parentSessionId: string;
129
+ parentLeaf: string | null;
130
+ createdAt: number;
131
+ options: AgentOptions;
132
+ alive: boolean;
133
+ };
134
+ read(id: string, owner: AgentOwner, cycleId?: string, offset?: number, limit?: number): Promise<{
135
+ cycles: {
136
+ cycleId: string;
137
+ state: "running" | "completed" | "failed" | "cancelled" | "interrupted";
138
+ }[];
139
+ report: string;
140
+ totalLength: number;
141
+ nextOffset: number | null;
142
+ cycle?: AgentCycle;
143
+ name: string;
144
+ generation: string;
145
+ state: import("../../model/agent.js").AgentStatus["state"];
146
+ activity: string;
147
+ error?: string;
148
+ owned: boolean;
149
+ agentId: string;
150
+ release: string;
151
+ workspace: string;
152
+ cwd: string;
153
+ parentSessionId: string;
154
+ parentLeaf: string | null;
155
+ createdAt: number;
156
+ options: AgentOptions;
157
+ alive: boolean;
158
+ }>;
159
+ completions(owner: AgentOwner): Promise<AgentCompletion[]>;
160
+ private report;
161
+ private release;
162
+ /** Called by the parent scanner; stop remains nonblocking to the model. */
163
+ refresh(owner: AgentOwner): Promise<void>;
164
+ close(owner?: AgentOwner, preserve?: boolean): Promise<void>;
165
+ private dispose;
166
+ }
@@ -0,0 +1,325 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { packageVersion, releaseLine } from "../../common/version.js";
5
+ import { requireThat } from "../../common/validation.js";
6
+ import { AgentFiles, descriptor, launch, status, cycle } from "../../storage/agent-files.js";
7
+ import { TeamFiles } from "../../storage/team-files.js";
8
+ export class AgentService {
9
+ root;
10
+ host;
11
+ files;
12
+ teamFiles;
13
+ #closed = false;
14
+ #preserve = false;
15
+ #starting = new Map();
16
+ #launches = new Set();
17
+ #closing;
18
+ #releasing = new Map();
19
+ constructor(root, directory, host) {
20
+ this.root = root;
21
+ this.host = host;
22
+ this.files = new AgentFiles(directory, host.kind);
23
+ this.teamFiles = new TeamFiles(this.files);
24
+ }
25
+ alive(id) { return this.#starting.has(id) || this.#releasing.has(id) || this.host.alive(id); }
26
+ check() { requireThat(!this.#closed, "子会话服务已关闭。"); }
27
+ owned(d, owner) {
28
+ return d.workspace === this.root && d.parentSessionId === owner.sessionId && (d.parentLeaf === null || owner.branch.includes(d.parentLeaf));
29
+ }
30
+ get(id, owner) {
31
+ this.check();
32
+ const d = this.files.read(id, "descriptor.json", descriptor);
33
+ requireThat(this.owned(d, owner), "子会话不属于当前会话分支。");
34
+ requireThat(releaseLine(d.release) === releaseLine(packageVersion), "子会话版本不兼容;请停止后使用匹配版本。");
35
+ return d;
36
+ }
37
+ records(owner) {
38
+ this.check();
39
+ return this.files.list().filter(d => d.workspace === this.root && d.parentSessionId === owner.sessionId &&
40
+ releaseLine(d.release) === releaseLine(packageVersion));
41
+ }
42
+ list(owner) { return this.records(owner).map(d => this.view(d, owner)); }
43
+ view(d, owner) {
44
+ const run = this.files.read(d.agentId, "launch.json", launch);
45
+ const saved = this.files.optional(d.agentId, "status.json", status);
46
+ const current = saved?.generation === run.generation ? saved : undefined;
47
+ const alive = this.alive(d.agentId), pending = this.#starting.get(d.agentId);
48
+ const starting = pending && !pending.controller.signal.aborted;
49
+ return { ...d, name: run.name, generation: run.generation, owned: this.owned(d, owner), alive,
50
+ state: alive ? starting ? "starting" : run.stop || pending?.controller.signal.aborted ? "stopping" : current?.state ?? "starting" : current?.state === "error" ? "error" : "stopped",
51
+ activity: starting ? "启动" : alive ? current?.activity ?? "启动" : current?.error ?? "已停止",
52
+ ...(current?.cycle ? { cycle: current.cycle } : {}), ...(current?.error ? { error: current.error } : {}) };
53
+ }
54
+ async start(options, owner) {
55
+ this.check();
56
+ this.host.available();
57
+ options = Object.fromEntries(Object.entries(options).filter(([, v]) => v !== undefined));
58
+ requireThat(options.provider && options.model, "请选择可在专家会话使用的模型。");
59
+ const prepared = this.files.control(() => {
60
+ requireThat(this.list(owner).filter(v => v.alive).length < 50, "最多 50 个存活或启动中子会话;请先停止一个空闲会话。");
61
+ const agentId = randomUUID();
62
+ const cwd = realpathSync(resolve(this.root, options.cwd ?? "."));
63
+ const files = (options.files ?? []).map(p => realpathSync(resolve(this.root, p)));
64
+ const d = { agentId, release: packageVersion, workspace: this.root, cwd,
65
+ parentSessionId: owner.sessionId, parentLeaf: owner.leaf, createdAt: Date.now(), options: { ...options, files, cwd } };
66
+ const run = { generation: randomUUID(), name: options.name ?? agentId.slice(0, 8), sequence: 0, stop: false, startedAt: Date.now() };
67
+ this.files.create(d, run);
68
+ const initial = [options.message, ...(options.prompts ?? [])].filter(Boolean).join("\n\n");
69
+ const request = initial || files.length ? this.files.enqueue(agentId, initial, "steer", files) : undefined;
70
+ this.#starting.set(agentId, { generation: run.generation, controller: new AbortController(), stage: "launch" });
71
+ return { d, run, request };
72
+ });
73
+ await this.launch(prepared.d, prepared.run.generation);
74
+ return { ...this.view(prepared.d, owner), ...(prepared.request ? { requestId: prepared.request.requestId, messageStatus: "queued" } : {}) };
75
+ }
76
+ async launch(d, generation, resume = false) {
77
+ const pending = this.#starting.get(d.agentId);
78
+ requireThat(pending?.generation === generation, "启动预约已失效。");
79
+ const operation = (async () => {
80
+ try {
81
+ await this.host.launch(d, this.files.directory(d.agentId), generation, this.files.reference(d.agentId), resume, pending.controller.signal);
82
+ requireThat(!this.#closed || this.#preserve, "子会话服务已关闭。");
83
+ pending.controller.signal.throwIfAborted();
84
+ const run = this.files.read(d.agentId, "launch.json", launch);
85
+ requireThat(run.generation === generation && !run.stop, "子会话启动已取消或代次变化。");
86
+ }
87
+ catch (error) {
88
+ const run = this.files.read(d.agentId, "launch.json", launch);
89
+ if (run.generation === generation) {
90
+ if (this.host.alive(d.agentId))
91
+ await this.release(d.agentId);
92
+ this.files.write(d.agentId, "status.json", { generation, state: run.stop ? "stopped" : "error",
93
+ activity: run.stop ? "已停止" : "启动失败", updatedAt: Date.now(), ...(run.stop ? {} : { error: String(error) }) }, status);
94
+ }
95
+ throw error;
96
+ }
97
+ finally {
98
+ if (this.#starting.get(d.agentId) === pending)
99
+ this.#starting.delete(d.agentId);
100
+ }
101
+ })();
102
+ this.#launches.add(operation);
103
+ try {
104
+ await operation;
105
+ }
106
+ finally {
107
+ this.#launches.delete(operation);
108
+ }
109
+ }
110
+ send(id, text, delivery, owner) {
111
+ const d = this.get(id, owner);
112
+ return this.files.control(() => {
113
+ requireThat(this.alive(id) && this.#starting.get(id)?.stage !== "validate" && !this.files.read(id, "launch.json", launch).stop, "子会话已停止或仍在恢复校验,请等待显式恢复完成。");
114
+ this.assertPrivate(id);
115
+ const request = this.files.enqueue(id, text, delivery);
116
+ return { ...this.view(d, owner), requestId: request.requestId, messageStatus: "queued" };
117
+ });
118
+ }
119
+ assertPrivate(id) {
120
+ this.teamFiles.check();
121
+ const run = this.files.read(id, "launch.json", launch), saved = this.files.optional(id, "status.json", status);
122
+ const complete = this.files.completions(id), alive = this.host.alive(id);
123
+ const occupied = this.teamFiles.list().some(t => t.members.some(m => m.agentId === id) && t.runs.some(r => r.state === "running" || r.issued.some(q => q.agentId === id && q.generation === run.generation && alive &&
124
+ saved?.generation === run.generation && saved.accepted?.includes(q.requestId) &&
125
+ !complete.some(c => c.generation === q.generation && c.requestIds.includes(q.requestId)))));
126
+ if (occupied)
127
+ throw Object.assign(new Error("TEAM_BUSY:请先暂停团队并等待在途工作结束。"), { code: "TEAM_BUSY" });
128
+ }
129
+ enqueueTeam(input, owner) {
130
+ const d = this.get(input.agentId, owner);
131
+ return this.files.control(() => {
132
+ this.teamFiles.check();
133
+ const team = this.teamFiles.read(input.teamId), run = team.runs.find(r => r.runId === input.runId);
134
+ const q = run?.issued.find(q => q.requestId === input.requestId), ready = this.teamFiles.ready(input.agentId);
135
+ const process = this.files.read(input.agentId, "launch.json", launch);
136
+ const valid = team.workspace === this.root && team.parentSessionId === owner.sessionId &&
137
+ (team.parentLeaf === null || owner.branch.includes(team.parentLeaf)) && !team.closed && run?.state === "running" &&
138
+ run.deadlineAt > Date.now() && q?.agentId === input.agentId && q.generation === input.generation &&
139
+ process.generation === input.generation && !process.stop && this.host.alive(input.agentId) &&
140
+ ready?.ready && ready.generation === input.generation;
141
+ if (!valid)
142
+ throw Object.assign(new Error("团队安排已失效;未重新执行。"), { code: "TEAM_INPUT_STALE" });
143
+ const request = this.files.enqueue(input.agentId, input.message, "followUp", [], input.requestId);
144
+ return { ...this.view(d, owner), requestId: request.requestId, messageStatus: "queued" };
145
+ });
146
+ }
147
+ rename(id, name, owner) {
148
+ requireThat(name.trim().length > 0 && name.length <= 64 && !/[\x00-\x1f]/.test(name), "名称必须是 1 到 64 字符的单行文本。");
149
+ const d = this.get(id, owner);
150
+ this.files.control(() => { const run = this.files.read(id, "launch.json", launch); run.name = name; this.files.write(id, "launch.json", run, launch); });
151
+ return this.view(d, owner);
152
+ }
153
+ stop(id, owner) {
154
+ const d = this.get(id, owner);
155
+ this.requestStop(id);
156
+ return this.view(d, owner);
157
+ }
158
+ requestStop(id) {
159
+ this.files.control(() => { const run = this.files.read(id, "launch.json", launch); run.stop = true; this.files.write(id, "launch.json", run, launch); });
160
+ this.#starting.get(id)?.controller.abort();
161
+ }
162
+ async enter(id, owner) {
163
+ const d = this.get(id, owner);
164
+ if (this.alive(id)) {
165
+ requireThat(!this.files.read(id, "launch.json", launch).stop, "子会话正在停止,请稍后进入。");
166
+ return this.view(d, owner);
167
+ }
168
+ return this.restart(id, owner);
169
+ }
170
+ resume(id, text, owner) {
171
+ return this.restart(id, owner, text);
172
+ }
173
+ async restart(id, owner, text) {
174
+ const d = this.get(id, owner);
175
+ this.host.available();
176
+ const reserved = this.files.control(() => {
177
+ requireThat(!this.alive(id), "子会话仍在运行。");
178
+ this.assertPrivate(id);
179
+ requireThat(this.list(owner).filter(v => v.alive).length < 50, "最多 50 个存活或启动中子会话。");
180
+ const previous = this.files.read(id, "launch.json", launch), interrupted = this.cycles(id, owner);
181
+ const pending = { generation: previous.generation, controller: new AbortController(), stage: "validate" };
182
+ this.#starting.set(id, pending);
183
+ return { previous, interrupted, pending };
184
+ });
185
+ const operation = (async () => {
186
+ await this.host.validateSession(d, this.files.reference(id));
187
+ requireThat(!this.#closed || this.#preserve, "子会话服务已关闭。");
188
+ reserved.pending.controller.signal.throwIfAborted();
189
+ const prepared = this.files.control(() => {
190
+ requireThat(!this.host.alive(id) && this.files.read(id, "launch.json", launch).generation === reserved.previous.generation, "子会话已由其他操作恢复。");
191
+ this.assertPrivate(id);
192
+ // Only the generation known to be stopped before validation can be sealed.
193
+ for (const ended of reserved.interrupted) {
194
+ if (ended.generation === reserved.previous.generation && !this.files.completions(id).some(c => c.cycleId === ended.cycleId)) {
195
+ this.files.write(id, "completions/" + ended.cycleId + ".json", ended, cycle, true);
196
+ }
197
+ }
198
+ this.#stopping.delete(id);
199
+ const run = { ...reserved.previous, generation: randomUUID(), sequence: 0, stop: false, startedAt: Date.now() };
200
+ this.files.write(id, "launch.json", run, launch);
201
+ const request = text === undefined ? undefined : this.files.enqueue(id, text, "steer");
202
+ reserved.pending.generation = run.generation;
203
+ reserved.pending.stage = "launch";
204
+ return { run, request };
205
+ });
206
+ await this.launch(d, prepared.run.generation, true);
207
+ return { ...this.view(d, owner), ...(prepared.request ? { requestId: prepared.request.requestId, messageStatus: "queued" } : {}) };
208
+ })();
209
+ this.#launches.add(operation);
210
+ try {
211
+ return await operation;
212
+ }
213
+ finally {
214
+ this.#launches.delete(operation);
215
+ if (this.#starting.get(id) === reserved.pending)
216
+ this.#starting.delete(id);
217
+ }
218
+ }
219
+ cycles(id, owner) {
220
+ const d = this.get(id, owner), view = this.view(d, owner), values = this.files.completions(id);
221
+ if (view.cycle && !values.some(c => c.cycleId === view.cycle.cycleId)) {
222
+ values.push(view.state === "stopped" || view.state === "error" ? { ...view.cycle, state: "interrupted", finishedAt: view.cycle.finishedAt ?? Date.now() } : view.cycle);
223
+ }
224
+ if (["stopped", "error"].includes(view.state)) {
225
+ const run = this.files.read(id, "launch.json", launch), saved = this.files.optional(id, "status.json", status);
226
+ // Preserve receipt-only crash windows before resume replaces the generation and status.
227
+ for (const requestId of saved?.generation === run.generation ? saved.accepted ?? [] : []) {
228
+ if (values.some(c => c.generation === run.generation && c.requestIds.includes(requestId)))
229
+ continue;
230
+ values.push({ cycleId: requestId, generation: run.generation, startedAt: run.startedAt, finishedAt: Date.now(),
231
+ state: "interrupted", leaf: null, requestIds: [requestId], source: "parent", error: "领取后执行结果不明;未重跑。" });
232
+ }
233
+ if (run.sequence > 0 && !values.some(c => c.generation === view.generation))
234
+ values.push({ cycleId: run.generation, generation: run.generation, startedAt: run.startedAt, finishedAt: run.startedAt,
235
+ state: "interrupted", leaf: null, requestIds: [], source: "parent", error: view.error ?? "子进程在交付前退出。" });
236
+ }
237
+ return values;
238
+ }
239
+ status(id, owner) {
240
+ const d = this.get(id, owner), latest = this.cycles(id, owner).at(-1);
241
+ return { ...this.view(d, owner), ...(latest ? { cycle: latest } : {}) };
242
+ }
243
+ async read(id, owner, cycleId, offset = 0, limit = 16000) {
244
+ const d = this.get(id, owner), cycles = this.cycles(id, owner);
245
+ const c = cycleId ? cycles.find(v => v.cycleId === cycleId) : cycles.at(-1);
246
+ requireThat(!cycleId || c, "周期不存在。");
247
+ const report = c ? await this.report(d, c) : "";
248
+ this.check();
249
+ requireThat(offset <= report.length, "分页位置超出报告。");
250
+ return { ...this.view(d, owner), ...(c ? { cycle: c } : {}), cycles: cycles.map(v => ({ cycleId: v.cycleId, state: v.state })),
251
+ report: report.slice(offset, offset + limit), totalLength: report.length, nextOffset: offset + limit < report.length ? offset + limit : null };
252
+ }
253
+ async completions(owner) {
254
+ 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) => ({ agentId: v.agentId, name: v.name, cycle: c, report: await this.report(v, c) }))));
255
+ this.check();
256
+ return values;
257
+ }
258
+ report(d, c) {
259
+ if (c.leaf === null)
260
+ return "";
261
+ return this.host.report(d, this.files.reference(d.agentId), c.leaf);
262
+ }
263
+ release(id) {
264
+ let pending = this.#releasing.get(id);
265
+ if (!pending) {
266
+ const generation = this.files.read(id, "launch.json", launch).generation;
267
+ pending = Promise.resolve().then(async () => {
268
+ if (this.files.read(id, "launch.json", launch).generation === generation)
269
+ await this.host.release(id, generation);
270
+ }).finally(() => this.#releasing.delete(id));
271
+ this.#releasing.set(id, pending);
272
+ }
273
+ return pending;
274
+ }
275
+ /** Called by the parent scanner; stop remains nonblocking to the model. */
276
+ async refresh(owner) {
277
+ const releases = [];
278
+ for (const d of this.records(owner)) {
279
+ const run = this.files.read(d.agentId, "launch.json", launch);
280
+ if (!run.stop)
281
+ this.#stopping.delete(d.agentId);
282
+ if (run.stop && this.alive(d.agentId)) {
283
+ this.#starting.get(d.agentId)?.controller.abort();
284
+ const since = this.#stopping.get(d.agentId) ?? Date.now();
285
+ this.#stopping.set(d.agentId, since);
286
+ if (Date.now() - since > 5000)
287
+ releases.push(this.release(d.agentId));
288
+ }
289
+ }
290
+ await Promise.all(releases);
291
+ }
292
+ #stopping = new Map();
293
+ close(owner, preserve = false) {
294
+ return this.#closing ??= this.dispose(owner, preserve);
295
+ }
296
+ async dispose(owner, preserve = false) {
297
+ if (this.#closed)
298
+ return;
299
+ const ids = !preserve && owner ? this.list(owner).filter(v => v.alive).map(v => v.agentId) : [];
300
+ this.#closed = true;
301
+ this.#preserve = preserve;
302
+ const errors = [];
303
+ for (const id of ids) {
304
+ try {
305
+ this.requestStop(id);
306
+ }
307
+ catch (error) {
308
+ errors.push(error);
309
+ }
310
+ }
311
+ if (!preserve)
312
+ for (const pending of this.#starting.values())
313
+ pending.controller.abort();
314
+ const deadline = Date.now() + 5000;
315
+ while (ids.some(id => this.alive(id)) && Date.now() < deadline)
316
+ await new Promise(r => setTimeout(r, 100));
317
+ const released = await Promise.allSettled(ids.filter(id => this.host.alive(id)).map(id => this.release(id)));
318
+ for (const value of released)
319
+ if (value.status === "rejected")
320
+ errors.push(value.reason);
321
+ await Promise.allSettled([...this.#launches]);
322
+ if (errors.length)
323
+ throw new AggregateError(errors, "停止或释放专家失败;请核对仍存活的执行资源。");
324
+ }
325
+ }