personal-bot 0.2.4 → 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 +9 -7
- package/README.md +13 -6
- package/README.zh-CN.md +13 -6
- package/docs/README.md +3 -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 +3 -1
- package/docs/interaction-design.md +13 -0
- package/docs/release.md +3 -3
- package/docs/technical-design.md +29 -15
- package/docs/validation.md +38 -2
- 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/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/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/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/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/npm-shrinkwrap.json +11140 -830
- package/package.json +94 -12
- package/python/README.md +6 -6
- package/resources/agents/coordinator.md +1 -1
- package/resources/dsh/cordis.patch.yml +3 -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 +9 -1
- 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/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/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/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
|
@@ -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
|
}
|