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.
- package/CONTEXT.md +13 -7
- package/README.md +46 -9
- package/README.zh-CN.md +46 -9
- package/bin/pbot.mjs +3 -0
- package/docs/README.md +4 -1
- package/docs/agent-collaboration.md +26 -12
- package/docs/architecture-guidelines.md +2 -2
- package/docs/capability-design.md +6 -4
- package/docs/dsh-implementation-plan.md +158 -0
- package/docs/dsh-support-proposal.md +314 -0
- package/docs/dsh.md +103 -0
- package/docs/file-storage.md +14 -9
- package/docs/history.md +29 -18
- package/docs/installation-design.md +226 -0
- package/docs/interaction-design.md +13 -0
- package/docs/release.md +5 -5
- package/docs/technical-design.md +29 -15
- package/docs/validation.md +53 -3
- package/lib/client.js +1071 -0
- package/lib/common/files.d.ts +2 -0
- package/lib/common/files.js +44 -0
- package/lib/common/json.d.ts +6 -0
- package/lib/common/json.js +16 -0
- package/lib/common/markdown.d.ts +5 -0
- package/lib/common/markdown.js +26 -0
- package/lib/common/package-source.js +16 -0
- package/lib/common/paths.d.ts +1 -0
- package/lib/common/paths.js +2 -0
- package/lib/common/validation.d.ts +3 -0
- package/lib/common/validation.js +14 -0
- package/lib/common/version.d.ts +3 -0
- package/lib/common/version.js +13 -0
- package/lib/dsh.d.ts +4 -0
- package/lib/dsh.js +22 -0
- package/lib/extension/dsh/agent-events.d.ts +24 -0
- package/lib/extension/dsh/agent-events.js +242 -0
- package/lib/extension/dsh/agent-runtime.d.ts +49 -0
- package/lib/extension/dsh/agent-runtime.js +348 -0
- package/lib/extension/dsh/command.d.ts +4 -0
- package/lib/extension/dsh/command.js +83 -0
- package/lib/extension/dsh/files.d.ts +5 -0
- package/lib/extension/dsh/files.js +67 -0
- package/lib/extension/dsh/history.d.ts +15 -0
- package/lib/extension/dsh/history.js +88 -0
- package/lib/extension/dsh/management.d.ts +21 -0
- package/lib/extension/dsh/management.js +217 -0
- package/lib/extension/dsh/workspace.d.ts +48 -0
- package/lib/extension/dsh/workspace.js +290 -0
- package/lib/extension/setup-cli.js +136 -0
- package/lib/extension/setup-guard.js +11 -0
- package/lib/integrations/agent-resources.d.ts +1 -0
- package/lib/integrations/agent-resources.js +18 -0
- package/lib/integrations/dsh/dsh-agents.d.ts +30 -0
- package/lib/integrations/dsh/dsh-agents.js +104 -0
- package/lib/integrations/dsh/history-summary.d.ts +11 -0
- package/lib/integrations/dsh/history-summary.js +40 -0
- package/lib/integrations/dsh/history.d.ts +16 -0
- package/lib/integrations/dsh/history.js +156 -0
- package/lib/integrations/dsh/input.d.ts +21 -0
- package/lib/integrations/dsh/input.js +62 -0
- package/lib/integrations/dsh/messages.d.ts +36 -0
- package/lib/integrations/dsh/messages.js +11 -0
- package/lib/integrations/dsh/notifications.d.ts +8 -0
- package/lib/integrations/dsh/notifications.js +55 -0
- package/lib/integrations/dsh/resources.d.ts +10 -0
- package/lib/integrations/dsh/resources.js +41 -0
- package/lib/integrations/dsh/schema.d.ts +7 -0
- package/lib/integrations/dsh/schema.js +28 -0
- package/lib/integrations/dsh/session.d.ts +14 -0
- package/lib/integrations/dsh/session.js +59 -0
- package/lib/integrations/history-summary.d.ts +2 -0
- package/lib/integrations/history-summary.js +21 -0
- package/lib/integrations/installation/pi-bridge.js +131 -0
- package/lib/integrations/installation/pi-setup.js +193 -0
- package/lib/integrations/installation/processes.js +155 -0
- package/lib/integrations/installation/workspace.js +21 -0
- package/lib/integrations/process.d.ts +14 -0
- package/lib/integrations/process.js +48 -0
- package/lib/integrations/python-activation.d.ts +2 -0
- package/lib/integrations/python-activation.js +29 -0
- package/lib/integrations/team-context.d.ts +18 -0
- package/lib/integrations/team-context.js +57 -0
- package/lib/model/agent.d.ts +92 -0
- package/lib/model/agent.js +1 -0
- package/lib/model/history.d.ts +43 -0
- package/lib/model/history.js +1 -0
- package/lib/model/team.d.ts +85 -0
- package/lib/model/team.js +1 -0
- package/lib/services/agents/agent-notices.d.ts +13 -0
- package/lib/services/agents/agent-notices.js +25 -0
- package/lib/services/agents/agent-run.d.ts +39 -0
- package/lib/services/agents/agent-run.js +138 -0
- package/lib/services/agents/agent-service.d.ts +166 -0
- package/lib/services/agents/agent-service.js +325 -0
- package/lib/services/agents/team-service.d.ts +168 -0
- package/lib/services/agents/team-service.js +343 -0
- package/lib/services/history/history-service.d.ts +103 -0
- package/lib/services/history/history-service.js +269 -0
- package/lib/services/installation/setup-service.js +121 -0
- package/lib/services/workspace/python-environment-service.d.ts +15 -0
- package/lib/services/workspace/python-environment-service.js +50 -0
- package/lib/services/workspace/workspace-files.d.ts +2 -0
- package/lib/services/workspace/workspace-files.js +17 -0
- package/lib/services/workspace/workspace-preparation.d.ts +2 -0
- package/lib/services/workspace/workspace-preparation.js +18 -0
- package/lib/storage/agent-files.d.ts +30 -0
- package/lib/storage/agent-files.js +166 -0
- package/lib/storage/atomic-json.d.ts +11 -0
- package/lib/storage/atomic-json.js +101 -0
- package/lib/storage/file-access.d.ts +2 -0
- package/lib/storage/file-access.js +26 -0
- package/lib/storage/history-files.d.ts +10 -0
- package/lib/storage/history-files.js +63 -0
- package/lib/storage/team-files.d.ts +16 -0
- package/lib/storage/team-files.js +111 -0
- package/lib/tools/agents/actions.d.ts +52 -0
- package/lib/tools/agents/actions.js +49 -0
- package/lib/tools/agents/contract.d.ts +163 -0
- package/lib/tools/agents/contract.js +56 -0
- package/lib/tools/agents/dsh-contract.d.ts +52 -0
- package/lib/tools/agents/dsh-contract.js +15 -0
- package/lib/tools/agents/dsh-tool.d.ts +5 -0
- package/lib/tools/agents/dsh-tool.js +38 -0
- package/lib/tools/history/actions.d.ts +70 -0
- package/lib/tools/history/actions.js +7 -0
- package/lib/tools/history/contract.d.ts +81 -0
- package/lib/tools/history/contract.js +32 -0
- package/lib/tools/history/dsh-tool.d.ts +4 -0
- package/lib/tools/history/dsh-tool.js +22 -0
- package/lib/tools/result.d.ts +18 -0
- package/lib/tools/result.js +10 -0
- package/lib/tools/teams/actions.d.ts +24 -0
- package/lib/tools/teams/actions.js +38 -0
- package/lib/tools/teams/contract.d.ts +232 -0
- package/lib/tools/teams/contract.js +67 -0
- package/lib/tools/teams/dsh-tool.d.ts +8 -0
- package/lib/tools/teams/dsh-tool.js +49 -0
- package/lib/ui/dsh/attachments.d.ts +5 -0
- package/lib/ui/dsh/attachments.js +39 -0
- package/lib/ui/dsh/elements.d.ts +17 -0
- package/lib/ui/dsh/elements.js +79 -0
- package/lib/ui/dsh/index.d.ts +4 -0
- package/lib/ui/dsh/index.js +13 -0
- package/lib/ui/dsh/management.d.ts +4 -0
- package/lib/ui/dsh/management.js +711 -0
- package/lib/ui/dsh/remote.d.ts +15 -0
- package/lib/ui/dsh/remote.js +11 -0
- package/lib/ui/dsh/styles.d.ts +1 -0
- package/lib/ui/dsh/styles.js +132 -0
- package/lib/ui/presentation.d.ts +3 -0
- package/lib/ui/presentation.js +12 -0
- package/lib/ui/setup.js +52 -0
- package/npm-shrinkwrap.json +11118 -783
- package/package.json +113 -9
- package/python/README.md +6 -6
- package/resources/agents/coordinator.md +1 -1
- package/resources/dsh/cordis.patch.yml +3 -0
- package/resources/installer/plugins.json +16 -0
- package/resources/prompts/workspace.md +1 -1
- package/resources/references/compute.md +1 -1
- package/resources/references/task-conductor.md +6 -4
- package/resources/skills/provider-connections/SKILL.md +10 -0
- package/scripts/README.md +13 -1
- package/src/common/package-source.ts +16 -0
- package/src/dsh.ts +22 -0
- package/src/extension/agent-events.ts +51 -46
- package/src/extension/agents-command.ts +48 -34
- package/src/extension/command.ts +42 -23
- package/src/extension/dsh/agent-events.ts +148 -0
- package/src/extension/dsh/agent-runtime.ts +251 -0
- package/src/extension/dsh/command.ts +78 -0
- package/src/extension/dsh/files.ts +61 -0
- package/src/extension/dsh/history.ts +69 -0
- package/src/extension/dsh/management.ts +108 -0
- package/src/extension/dsh/workspace.ts +206 -0
- package/src/extension/events.ts +6 -4
- package/src/extension/history.ts +15 -12
- package/src/extension/setup-cli.ts +109 -0
- package/src/extension/setup-guard.ts +12 -0
- package/src/extension/subagent-entry.ts +34 -63
- package/src/extension/teams-command.ts +53 -46
- package/src/integrations/{pi/agent-resources.ts → agent-resources.ts} +2 -2
- package/src/integrations/dsh/dsh-agents.ts +97 -0
- package/src/integrations/dsh/history-summary.ts +43 -0
- package/src/integrations/dsh/history.ts +76 -0
- package/src/integrations/dsh/input.ts +66 -0
- package/src/integrations/dsh/messages.ts +22 -0
- package/src/integrations/dsh/notifications.ts +46 -0
- package/src/integrations/dsh/resources.ts +44 -0
- package/src/integrations/dsh/schema.ts +26 -0
- package/src/integrations/dsh/session.ts +67 -0
- package/src/integrations/history-summary.ts +20 -0
- package/src/integrations/installation/pi-bridge.ts +107 -0
- package/src/integrations/installation/pi-setup.ts +162 -0
- package/src/integrations/installation/processes.ts +124 -0
- package/src/integrations/installation/workspace.ts +18 -0
- package/src/integrations/pi/history.ts +4 -14
- package/src/integrations/pi/pi-agents.ts +17 -6
- package/src/integrations/pi/team-context.ts +6 -58
- package/src/integrations/team-context.ts +60 -0
- package/src/model/agent.ts +2 -0
- package/src/model/history.ts +3 -1
- package/src/pbot.ts +1 -1
- package/src/services/agents/agent-notices.ts +28 -0
- package/src/services/agents/agent-run.ts +114 -0
- package/src/services/agents/agent-service.ts +127 -64
- package/src/services/history/history-service.ts +120 -59
- package/src/services/installation/setup-service.ts +145 -0
- package/src/storage/agent-files.ts +17 -4
- package/src/storage/history-files.ts +30 -5
- package/src/tools/agents/actions.ts +47 -0
- package/src/tools/agents/contract.ts +8 -5
- package/src/tools/agents/dsh-contract.ts +16 -0
- package/src/tools/agents/dsh-tool.ts +41 -0
- package/src/tools/agents/tool.ts +7 -43
- package/src/tools/history/actions.ts +9 -0
- package/src/tools/history/contract.ts +34 -0
- package/src/tools/history/dsh-tool.ts +22 -0
- package/src/tools/history/tool.ts +7 -30
- package/src/tools/teams/actions.ts +38 -0
- package/src/tools/teams/contract.ts +7 -3
- package/src/tools/teams/dsh-tool.ts +55 -0
- package/src/tools/teams/tool.ts +6 -31
- package/src/ui/agent-display.ts +7 -4
- package/src/ui/dsh/attachments.ts +42 -0
- package/src/ui/dsh/elements.ts +50 -0
- package/src/ui/dsh/index.ts +18 -0
- package/src/ui/dsh/management.ts +412 -0
- package/src/ui/dsh/remote.ts +20 -0
- package/src/ui/dsh/styles.ts +132 -0
- package/src/ui/pi/menu.ts +37 -0
- package/src/ui/pi/reader.ts +46 -0
- package/src/ui/presentation.ts +12 -0
- package/src/ui/setup.ts +40 -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
|
+
}
|