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,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,
|
|
15
|
-
launch(value: AgentDescriptor, directory: string, generation: string,
|
|
16
|
-
|
|
17
|
-
report(value: AgentDescriptor,
|
|
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.
|
|
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
|
-
|
|
56
|
-
requireThat(this.list(owner).filter(v =>
|
|
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
|
-
|
|
67
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
136
|
-
requireThat(!this.
|
|
164
|
+
const reserved = this.files.control(() => {
|
|
165
|
+
requireThat(!this.alive(id), "子会话仍在运行。");
|
|
137
166
|
this.assertPrivate(id);
|
|
138
|
-
requireThat(this.list(owner).filter(v =>
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
this.#
|
|
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
|
}
|