personal-bot 0.1.2 → 0.2.1
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 +6 -2
- package/README.md +9 -7
- package/README.zh-CN.md +9 -7
- package/docs/README.md +3 -1
- package/docs/agent-collaboration.md +98 -40
- package/docs/file-storage.md +25 -4
- package/docs/release.md +2 -2
- package/docs/subagent-upgrade-proposal.md +357 -0
- package/docs/technical-design.md +23 -4
- package/docs/validation.md +35 -5
- package/npm-shrinkwrap.json +2253 -0
- package/package.json +5 -4
- package/resources/agents/coordinator.md +8 -1
- package/resources/agents/general-purpose.md +10 -4
- package/resources/prompts/tool-guidelines.md +2 -2
- package/resources/prompts/workspace.md +2 -2
- package/resources/references/compute.md +3 -3
- package/resources/references/task-conductor.md +4 -4
- package/resources/skills/project-maintenance/SKILL.md +1 -1
- package/resources/skills/provider-connections/SKILL.md +2 -2
- package/src/common/markdown.ts +26 -0
- package/src/extension/agent-events.ts +116 -14
- package/src/extension/agents-command.ts +15 -17
- package/src/extension/command.ts +5 -3
- package/src/extension/lifecycle.ts +7 -2
- package/src/extension/subagent-entry.ts +144 -31
- package/src/extension/teams-command.ts +60 -0
- package/src/integrations/pi/agent-context.ts +1 -1
- package/src/integrations/pi/agent-input.ts +19 -0
- package/src/integrations/pi/pi-agents.ts +22 -9
- package/src/integrations/pi/team-context.ts +78 -0
- package/src/model/agent.ts +2 -0
- package/src/model/team.ts +29 -0
- package/src/pbot.ts +5 -3
- package/src/services/agents/agent-service.ts +91 -21
- package/src/services/agents/team-service.ts +274 -0
- package/src/storage/agent-files.ts +43 -10
- package/src/storage/team-files.ts +93 -0
- package/src/tools/agents/contract.ts +8 -6
- package/src/tools/agents/tool.ts +10 -9
- package/src/tools/register.ts +6 -3
- package/src/tools/result.ts +5 -0
- package/src/tools/teams/contract.ts +67 -0
- package/src/tools/teams/tool.ts +63 -0
|
@@ -4,19 +4,25 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import { packageVersion, releaseLine } from "../../common/version.js";
|
|
5
5
|
import { requireThat } from "../../common/validation.js";
|
|
6
6
|
import { AgentFiles, descriptor, launch, status, cycle } from "../../storage/agent-files.js";
|
|
7
|
-
import
|
|
7
|
+
import { TeamFiles } from "../../storage/team-files.js";
|
|
8
|
+
import type { TeamAgent, TeamInput } from "../../model/team.js";
|
|
9
|
+
import type { AgentOwner, AgentOptions, AgentDescriptor, AgentCycle, AgentCompletion } from "../../model/agent.js";
|
|
8
10
|
|
|
9
11
|
export interface AgentHost {
|
|
10
12
|
available(): void;
|
|
11
13
|
alive(id: string): boolean;
|
|
12
|
-
|
|
14
|
+
validateSession(value: AgentDescriptor, file: string): void;
|
|
15
|
+
launch(value: AgentDescriptor, directory: string, generation: string, sessionFile: string | null): void;
|
|
13
16
|
kill(id: string): void;
|
|
14
|
-
report(file: string, leaf: string
|
|
17
|
+
report(value: AgentDescriptor, file: string, leaf: string): string;
|
|
15
18
|
}
|
|
16
19
|
export class AgentService {
|
|
17
20
|
readonly files: AgentFiles;
|
|
21
|
+
readonly teamFiles: TeamFiles;
|
|
18
22
|
#closed = false;
|
|
19
|
-
constructor(readonly root: string, directory: string, readonly host: AgentHost) {
|
|
23
|
+
constructor(readonly root: string, directory: string, readonly host: AgentHost) {
|
|
24
|
+
this.files = new AgentFiles(directory); this.teamFiles = new TeamFiles(this.files);
|
|
25
|
+
}
|
|
20
26
|
private check() { requireThat(!this.#closed, "子会话服务已关闭。"); }
|
|
21
27
|
private owned(d: AgentDescriptor, owner: AgentOwner) {
|
|
22
28
|
return d.workspace === this.root && d.parentSessionId === owner.sessionId && (d.parentLeaf === null || owner.branch.includes(d.parentLeaf));
|
|
@@ -26,15 +32,18 @@ export class AgentService {
|
|
|
26
32
|
requireThat(this.owned(d, owner), "子会话不属于当前会话分支。");
|
|
27
33
|
requireThat(releaseLine(d.release) === releaseLine(packageVersion), "子会话版本不兼容;请停止后使用匹配版本。"); return d;
|
|
28
34
|
}
|
|
29
|
-
|
|
30
|
-
this.check();
|
|
35
|
+
private records(owner: AgentOwner) {
|
|
36
|
+
this.check();
|
|
37
|
+
return this.files.list().filter(d => d.workspace === this.root && d.parentSessionId === owner.sessionId &&
|
|
38
|
+
releaseLine(d.release) === releaseLine(packageVersion));
|
|
31
39
|
}
|
|
32
|
-
|
|
40
|
+
list(owner: AgentOwner): TeamAgent[] { return this.records(owner).map(d => this.view(d, owner)); }
|
|
41
|
+
private view(d: AgentDescriptor, owner: AgentOwner): TeamAgent {
|
|
33
42
|
const run = this.files.read(d.agentId, "launch.json", launch);
|
|
34
43
|
const saved = this.files.optional(d.agentId, "status.json", status);
|
|
35
44
|
const current = saved?.generation === run.generation ? saved : undefined;
|
|
36
45
|
const alive = this.host.alive(d.agentId);
|
|
37
|
-
return { ...d, name: run.name, generation: run.generation, owned: this.owned(d, owner),
|
|
46
|
+
return { ...d, name: run.name, generation: run.generation, owned: this.owned(d, owner), alive,
|
|
38
47
|
state: alive ? run.stop ? "stopping" : current?.state ?? "starting" : current?.state === "error" ? "error" : "stopped",
|
|
39
48
|
activity: alive ? current?.activity ?? "启动" : current?.error ?? "已停止",
|
|
40
49
|
...(current?.cycle ? { cycle: current.cycle } : {}), ...(current?.error ? { error: current.error } : {}) };
|
|
@@ -52,23 +61,52 @@ export class AgentService {
|
|
|
52
61
|
parentSessionId: owner.sessionId, parentLeaf: owner.leaf, createdAt: Date.now(), options: { ...options, files, cwd } };
|
|
53
62
|
const run = { generation: randomUUID(), name: options.name ?? agentId.slice(0, 8), sequence: 0, stop: false, startedAt: Date.now() };
|
|
54
63
|
this.files.create(d, run);
|
|
55
|
-
const
|
|
56
|
-
|
|
64
|
+
const initial = [options.message, ...(options.prompts ?? [])].filter(Boolean).join("\n\n");
|
|
65
|
+
const request = initial || files.length ? this.files.enqueue(agentId, initial, "steer", files) : undefined;
|
|
66
|
+
try { this.host.launch(d, this.files.directory(agentId), run.generation, this.files.session(agentId)); }
|
|
57
67
|
catch (error) {
|
|
58
68
|
this.files.write(agentId, "status.json", { generation: run.generation, state: "error", activity: "启动失败", updatedAt: Date.now(), error: String(error) }, status);
|
|
59
69
|
throw error;
|
|
60
70
|
}
|
|
61
|
-
return { ...this.view(d, owner), requestId: request.requestId, messageStatus: "queued" as const };
|
|
71
|
+
return { ...this.view(d, owner), ...(request ? { requestId: request.requestId, messageStatus: "queued" as const } : {}) };
|
|
62
72
|
});
|
|
63
73
|
}
|
|
64
74
|
send(id: string, text: string, delivery: "steer" | "followUp", owner: AgentOwner) {
|
|
65
75
|
const d = this.get(id, owner);
|
|
66
76
|
return this.files.control(() => {
|
|
67
77
|
requireThat(this.host.alive(id) && !this.files.read(id, "launch.json", launch).stop, "子会话已停止,请显式 resume。");
|
|
78
|
+
this.assertPrivate(id);
|
|
68
79
|
const request = this.files.enqueue(id, text, delivery);
|
|
69
80
|
return { ...this.view(d, owner), requestId: request.requestId, messageStatus: "queued" as const };
|
|
70
81
|
});
|
|
71
82
|
}
|
|
83
|
+
private assertPrivate(id: string) {
|
|
84
|
+
this.teamFiles.check();
|
|
85
|
+
const run = this.files.read(id, "launch.json", launch), saved = this.files.optional(id, "status.json", status);
|
|
86
|
+
const complete = this.files.completions(id), alive = this.host.alive(id);
|
|
87
|
+
const occupied = this.teamFiles.list().some(t => t.members.some(m => m.agentId === id) && t.runs.some(r =>
|
|
88
|
+
r.state === "running" || r.issued.some(q => q.agentId === id && q.generation === run.generation && alive &&
|
|
89
|
+
saved?.generation === run.generation && saved.accepted?.includes(q.requestId) &&
|
|
90
|
+
!complete.some(c => c.generation === q.generation && c.requestIds.includes(q.requestId)))));
|
|
91
|
+
if (occupied) throw Object.assign(new Error("TEAM_BUSY:请先暂停团队并等待在途工作结束。"), { code: "TEAM_BUSY" });
|
|
92
|
+
}
|
|
93
|
+
enqueueTeam(input: TeamInput, owner: AgentOwner) {
|
|
94
|
+
const d = this.get(input.agentId, owner);
|
|
95
|
+
return this.files.control(() => {
|
|
96
|
+
this.teamFiles.check();
|
|
97
|
+
const team = this.teamFiles.read(input.teamId), run = team.runs.find(r => r.runId === input.runId);
|
|
98
|
+
const q = run?.issued.find(q => q.requestId === input.requestId), ready = this.teamFiles.ready(input.agentId);
|
|
99
|
+
const process = this.files.read(input.agentId, "launch.json", launch);
|
|
100
|
+
const valid = team.workspace === this.root && team.parentSessionId === owner.sessionId &&
|
|
101
|
+
(team.parentLeaf === null || owner.branch.includes(team.parentLeaf)) && !team.closed && run?.state === "running" &&
|
|
102
|
+
run.deadlineAt > Date.now() && q?.agentId === input.agentId && q.generation === input.generation &&
|
|
103
|
+
process.generation === input.generation && !process.stop && this.host.alive(input.agentId) &&
|
|
104
|
+
ready?.ready && ready.generation === input.generation;
|
|
105
|
+
if (!valid) throw Object.assign(new Error("团队安排已失效;未重新执行。"), { code: "TEAM_INPUT_STALE" });
|
|
106
|
+
const request = this.files.enqueue(input.agentId, input.message, "followUp", [], input.requestId);
|
|
107
|
+
return { ...this.view(d, owner), requestId: request.requestId, messageStatus: "queued" as const };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
72
110
|
rename(id: string, name: string, owner: AgentOwner) {
|
|
73
111
|
requireThat(name.trim().length > 0 && name.length <= 64 && !/[\x00-\x1f]/.test(name), "名称必须是 1 到 64 字符的单行文本。");
|
|
74
112
|
const d = this.get(id, owner);
|
|
@@ -81,11 +119,25 @@ export class AgentService {
|
|
|
81
119
|
private requestStop(id: string) {
|
|
82
120
|
this.files.control(() => { const run = this.files.read(id, "launch.json", launch); run.stop = true; this.files.write(id, "launch.json", run, launch); });
|
|
83
121
|
}
|
|
122
|
+
enter(id: string, owner: AgentOwner) {
|
|
123
|
+
const d = this.get(id, owner);
|
|
124
|
+
if (this.host.alive(id)) {
|
|
125
|
+
requireThat(!this.files.read(id, "launch.json", launch).stop, "子会话正在停止,请稍后进入。");
|
|
126
|
+
return this.view(d, owner);
|
|
127
|
+
}
|
|
128
|
+
return this.restart(id, owner);
|
|
129
|
+
}
|
|
84
130
|
resume(id: string, text: string, owner: AgentOwner) {
|
|
131
|
+
return this.restart(id, owner, text);
|
|
132
|
+
}
|
|
133
|
+
private restart(id: string, owner: AgentOwner, text?: string) {
|
|
85
134
|
const d = this.get(id, owner); this.host.available();
|
|
86
135
|
return this.files.control(() => {
|
|
87
136
|
requireThat(!this.host.alive(id), "子会话仍在运行。");
|
|
137
|
+
this.assertPrivate(id);
|
|
88
138
|
requireThat(this.list(owner).filter(v => this.host.alive(v.agentId)).length < 50, "最多 50 个存活子会话。");
|
|
139
|
+
const sessionFile = this.files.session(id);
|
|
140
|
+
if (sessionFile !== null) this.host.validateSession(d, sessionFile);
|
|
89
141
|
const previous = this.files.read(id, "launch.json", launch);
|
|
90
142
|
// The old process is gone: seal its interruption before replacing its generation.
|
|
91
143
|
for (const ended of this.cycles(id, owner)) {
|
|
@@ -96,9 +148,9 @@ export class AgentService {
|
|
|
96
148
|
this.#stopping.delete(id);
|
|
97
149
|
const run = { ...previous, generation: randomUUID(), sequence: 0, stop: false, startedAt: Date.now() };
|
|
98
150
|
this.files.write(id, "launch.json", run, launch);
|
|
99
|
-
const request = this.files.enqueue(id, text, "steer");
|
|
100
|
-
this.host.launch(d, this.files.directory(id), run.generation);
|
|
101
|
-
return { ...this.view(d, owner), requestId: request.requestId, messageStatus: "queued" as const };
|
|
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 } : {}) };
|
|
102
154
|
});
|
|
103
155
|
}
|
|
104
156
|
cycles(id: string, owner: AgentOwner): AgentCycle[] {
|
|
@@ -106,29 +158,45 @@ export class AgentService {
|
|
|
106
158
|
if (view.cycle && !values.some(c => c.cycleId === view.cycle!.cycleId)) {
|
|
107
159
|
values.push(view.state === "stopped" || view.state === "error" ? { ...view.cycle, state: "interrupted", finishedAt: view.cycle.finishedAt ?? Date.now() } : view.cycle);
|
|
108
160
|
}
|
|
109
|
-
if (
|
|
110
|
-
const run = this.files.read(id, "launch.json", launch);
|
|
111
|
-
|
|
161
|
+
if (["stopped", "error"].includes(view.state)) {
|
|
162
|
+
const run = this.files.read(id, "launch.json", launch), saved = this.files.optional(id, "status.json", status);
|
|
163
|
+
// Preserve receipt-only crash windows before resume replaces the generation and status.
|
|
164
|
+
for (const requestId of saved?.generation === run.generation ? saved.accepted ?? [] : []) {
|
|
165
|
+
if (values.some(c => c.generation === run.generation && c.requestIds.includes(requestId))) continue;
|
|
166
|
+
values.push({ cycleId: requestId, generation: run.generation, startedAt: run.startedAt, finishedAt: Date.now(),
|
|
167
|
+
state: "interrupted", leaf: null, requestIds: [requestId], source: "parent", error: "领取后执行结果不明;未重跑。" });
|
|
168
|
+
}
|
|
169
|
+
if (run.sequence > 0 && !values.some(c => c.generation === view.generation)) values.push({ cycleId: run.generation, generation: run.generation, startedAt: run.startedAt, finishedAt: run.startedAt,
|
|
112
170
|
state: "interrupted", leaf: null, requestIds: [], source: "parent", error: view.error ?? "子进程在交付前退出。" });
|
|
113
171
|
}
|
|
114
172
|
return values;
|
|
115
173
|
}
|
|
174
|
+
status(id: string, owner: AgentOwner) {
|
|
175
|
+
const d = this.get(id, owner), latest = this.cycles(id, owner).at(-1);
|
|
176
|
+
return { ...this.view(d, owner), ...(latest ? { cycle: latest } : {}) };
|
|
177
|
+
}
|
|
116
178
|
read(id: string, owner: AgentOwner, cycleId?: string, offset = 0, limit = 16000) {
|
|
117
179
|
const d = this.get(id, owner), cycles = this.cycles(id, owner);
|
|
118
180
|
const c = cycleId ? cycles.find(v => v.cycleId === cycleId) : cycles.at(-1);
|
|
119
181
|
requireThat(!cycleId || c, "周期不存在。");
|
|
120
|
-
const report = c ? this.
|
|
182
|
+
const report = c ? this.report(d, c) : "";
|
|
121
183
|
requireThat(offset <= report.length, "分页位置超出报告。");
|
|
122
184
|
return { ...this.view(d, owner), ...(c ? { cycle: c } : {}), cycles: cycles.map(v => ({ cycleId: v.cycleId, state: v.state })),
|
|
123
185
|
report: report.slice(offset, offset + limit), totalLength: report.length, nextOffset: offset + limit < report.length ? offset + limit : null };
|
|
124
186
|
}
|
|
125
187
|
completions(owner: AgentOwner): AgentCompletion[] {
|
|
126
188
|
return this.list(owner).filter(v => v.owned).flatMap(v => this.cycles(v.agentId, owner).filter(c => c.state !== "running").map(c =>
|
|
127
|
-
({ agentId: v.agentId, name: v.name, cycle: c, report: this.
|
|
189
|
+
({ agentId: v.agentId, name: v.name, cycle: c, report: this.report(v, c) })));
|
|
190
|
+
}
|
|
191
|
+
private report(d: AgentDescriptor, c: AgentCycle) {
|
|
192
|
+
if (c.leaf === null) return "";
|
|
193
|
+
const file = this.files.session(d.agentId);
|
|
194
|
+
requireThat(file !== null, "完成记录缺少原生会话文件引用。");
|
|
195
|
+
return this.host.report(d, file, c.leaf);
|
|
128
196
|
}
|
|
129
197
|
/** Called by the parent scanner; stop remains nonblocking to the model. */
|
|
130
198
|
refresh(owner: AgentOwner) {
|
|
131
|
-
for (const d of this.
|
|
199
|
+
for (const d of this.records(owner)) {
|
|
132
200
|
const run = this.files.read(d.agentId, "launch.json", launch);
|
|
133
201
|
// A stopped request gets its own grace deadline without modifying child status.
|
|
134
202
|
if (!run.stop) this.#stopping.delete(d.agentId);
|
|
@@ -143,10 +211,12 @@ export class AgentService {
|
|
|
143
211
|
if (this.#closed) return;
|
|
144
212
|
if (!preserve && owner) {
|
|
145
213
|
const ids = this.list(owner).filter(v => this.host.alive(v.agentId)).map(v => v.agentId);
|
|
146
|
-
|
|
214
|
+
const errors: unknown[] = [];
|
|
215
|
+
for (const id of ids) { try { this.requestStop(id); } catch (error) { errors.push(error); } }
|
|
147
216
|
const deadline = Date.now() + 5000;
|
|
148
217
|
while (ids.some(id => this.host.alive(id)) && Date.now() < deadline) await new Promise(r => setTimeout(r, 100));
|
|
149
218
|
for (const id of ids) if (this.host.alive(id)) this.host.kill(id);
|
|
219
|
+
if (errors.length) { this.#closed = true; throw new AggregateError(errors, "停止记录未能保存;已清理子进程。"); }
|
|
150
220
|
}
|
|
151
221
|
this.#closed = true;
|
|
152
222
|
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { requireThat } from "../../common/validation.js";
|
|
3
|
+
import { packageVersion, releaseLine } from "../../common/version.js";
|
|
4
|
+
import { AgentFiles, descriptor, launch, status, message } from "../../storage/agent-files.js";
|
|
5
|
+
import { TeamFiles } from "../../storage/team-files.js";
|
|
6
|
+
import type { AgentCompletion, AgentOwner } from "../../model/agent.js";
|
|
7
|
+
import type { TeamActor, TeamAgent, TeamInput, TeamLink, TeamMessage, TeamRecord, TeamRunOptions } from "../../model/team.js";
|
|
8
|
+
|
|
9
|
+
/** Team decisions only. The parent dispatches returned inputs through AgentService. */
|
|
10
|
+
export class TeamService {
|
|
11
|
+
#failure: unknown;
|
|
12
|
+
#disposed = false;
|
|
13
|
+
readonly #recovered = new Set<string>();
|
|
14
|
+
constructor(readonly root: string, readonly agents: AgentFiles, readonly files: TeamFiles) {}
|
|
15
|
+
check() { requireThat(!this.#disposed, "团队服务已关闭。"); this.files.check(); if (this.#failure) throw this.#failure; }
|
|
16
|
+
fail(error: unknown) { this.#failure = error; }
|
|
17
|
+
dispose() { this.#disposed = true; }
|
|
18
|
+
private owned(team: TeamRecord, owner: AgentOwner) {
|
|
19
|
+
return team.workspace === this.root && team.parentSessionId === owner.sessionId && (team.parentLeaf === null || owner.branch.includes(team.parentLeaf));
|
|
20
|
+
}
|
|
21
|
+
private records(sessionId?: string) {
|
|
22
|
+
this.check();
|
|
23
|
+
return this.files.list().filter(t => t.workspace === this.root && releaseLine(t.release) === releaseLine(packageVersion) &&
|
|
24
|
+
(!sessionId || t.parentSessionId === sessionId));
|
|
25
|
+
}
|
|
26
|
+
private get(id: string, actor: TeamActor) {
|
|
27
|
+
this.check(); const team = this.files.read(id);
|
|
28
|
+
requireThat(team.workspace === this.root && releaseLine(team.release) === releaseLine(packageVersion), "团队工作区或版本不匹配。");
|
|
29
|
+
if (actor.kind === "member") {
|
|
30
|
+
const d = this.agents.read(actor.agentId, "descriptor.json", descriptor), run = this.agents.read(actor.agentId, "launch.json", launch);
|
|
31
|
+
requireThat(d.workspace === this.root && d.parentSessionId === team.parentSessionId && team.members.some(m => m.agentId === actor.agentId) &&
|
|
32
|
+
run.generation === actor.generation && !run.stop, "团队成员身份已失效。");
|
|
33
|
+
} else requireThat(this.owned(team, actor.owner), "团队不属于当前会话分支。");
|
|
34
|
+
return team;
|
|
35
|
+
}
|
|
36
|
+
private member(id: string, owner: AgentOwner, views: TeamAgent[]) {
|
|
37
|
+
const v = views.find(v => v.agentId === id), d = this.agents.read(id, "descriptor.json", descriptor);
|
|
38
|
+
requireThat(v && v.owned && d.workspace === this.root && d.parentSessionId === owner.sessionId &&
|
|
39
|
+
(d.parentLeaf === null || owner.branch.includes(d.parentLeaf)), "成员不属于当前分支:" + id);
|
|
40
|
+
requireThat(releaseLine(d.release) === releaseLine(packageVersion), "成员版本不兼容:" + id);
|
|
41
|
+
return v;
|
|
42
|
+
}
|
|
43
|
+
private requests(team: TeamRecord, views: TeamAgent[]) {
|
|
44
|
+
const states = new Map(team.members.map(m => [m.agentId, { view: views.find(v => v.agentId === m.agentId),
|
|
45
|
+
saved: this.agents.optional(m.agentId, "status.json", status),
|
|
46
|
+
cycles: new Map(this.agents.completions(m.agentId).flatMap(c => c.requestIds.map(id => [c.generation + ":" + id, c] as const))) }]));
|
|
47
|
+
return team.runs.flatMap(run => run.issued.map(q => {
|
|
48
|
+
const s = states.get(q.agentId)!, ended = s.cycles.get(q.generation + ":" + q.requestId);
|
|
49
|
+
const accepted = s.saved?.generation === q.generation && !!s.saved.accepted?.includes(q.requestId);
|
|
50
|
+
const active = !ended && accepted && !!s.view?.alive && s.view.generation === q.generation;
|
|
51
|
+
let outcome: string | undefined = ended?.state;
|
|
52
|
+
if (ended && ended.source !== "parent") outcome = ended.source;
|
|
53
|
+
else if (outcome === "completed" && !team.messages.some(m => m.source?.requestId === q.requestId)) outcome = "unpublished";
|
|
54
|
+
return { requestId: q.requestId, agentId: q.agentId, runId: run.runId, round: q.round, active,
|
|
55
|
+
state: outcome ?? (active ? "accepted" : accepted ? "uncertain" : run.state === "running" ? "queued" : "cancelled"),
|
|
56
|
+
...(ended ? { cycleId: ended.cycleId } : {}) };
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
private active(team: TeamRecord, views: TeamAgent[]) { return this.requests(team, views).filter(q => q.active); }
|
|
60
|
+
private available(id: string, owner: AgentOwner, views: TeamAgent[], links: Map<string, TeamLink>) {
|
|
61
|
+
const v = this.member(id, owner, views), run = this.agents.read(id, "launch.json", launch);
|
|
62
|
+
const saved = this.agents.optional(id, "status.json", status), ready = this.files.ready(id);
|
|
63
|
+
const available = (condition: unknown, message: string): void => {
|
|
64
|
+
if (!condition) throw Object.assign(new Error(message), { code: "TEAM_NOT_READY" });
|
|
65
|
+
};
|
|
66
|
+
available(v.alive && v.state === "idle" && !run.stop && v.generation === run.generation && saved?.generation === run.generation && saved.state === "idle", "成员尚未空闲:" + id);
|
|
67
|
+
available(ready?.ready && ready.generation === run.generation, "成员需加载新版子入口:" + id);
|
|
68
|
+
available(!this.agents.messages(id).some(q => q.generation === run.generation && !saved?.accepted?.includes(q.requestId) && !links.has(q.requestId)), "成员有待处理私人输入:" + id);
|
|
69
|
+
return v;
|
|
70
|
+
}
|
|
71
|
+
create(owner: AgentOwner, input: Pick<TeamRecord, "name" | "brief" | "members">, views: TeamAgent[]) {
|
|
72
|
+
this.check();
|
|
73
|
+
return this.agents.control(() => {
|
|
74
|
+
const records = this.records();
|
|
75
|
+
for (const m of input.members) {
|
|
76
|
+
this.member(m.agentId, owner, views);
|
|
77
|
+
requireThat(!records.some(t => !t.closed && t.members.some(v => v.agentId === m.agentId)), "成员已属于未关闭团队:" + m.agentId);
|
|
78
|
+
}
|
|
79
|
+
const team: TeamRecord = { ...input, members: structuredClone(input.members), teamId: randomUUID(), release: packageVersion,
|
|
80
|
+
workspace: this.root, parentSessionId: owner.sessionId, parentLeaf: owner.leaf, closed: false, runs: [], messages: [] };
|
|
81
|
+
this.files.save(team, true); return team;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
run(id: string, owner: AgentOwner, options: TeamRunOptions, views: TeamAgent[]) {
|
|
85
|
+
this.check();
|
|
86
|
+
return this.agents.control(() => {
|
|
87
|
+
const team = this.get(id, { kind: "coordinator", owner }), links = this.files.links(this.records());
|
|
88
|
+
requireThat(!team.closed && !team.runs.some(r => r.state === "running") && !this.active(team, views).length, "团队仍在工作或已关闭。");
|
|
89
|
+
const members = team.members.map(m => this.available(m.agentId, owner, views, links));
|
|
90
|
+
requireThat(options.mode !== "roundRobin" || options.maxConcurrent === undefined || options.maxConcurrent === 1, "轮流讨论并发须为 1。");
|
|
91
|
+
const rounds = options.rounds ?? 2, maxConcurrent = options.mode === "roundRobin" ? 1 : options.maxConcurrent ?? Math.min(2, members.length);
|
|
92
|
+
requireThat(Number.isInteger(rounds) && rounds >= 1 && rounds <= 10 && Number.isInteger(maxConcurrent) && maxConcurrent >= 1 && maxConcurrent <= members.length, "轮次或并发无效。");
|
|
93
|
+
const seconds = options.timeoutSeconds ?? 900;
|
|
94
|
+
requireThat(Number.isInteger(seconds) && seconds >= 1 && seconds <= 86400, "期限须为 1–86400 秒。");
|
|
95
|
+
requireThat(options.instruction.trim().length > 0, "新批次需要新的明确要求。");
|
|
96
|
+
const run = { runId: randomUUID(), instruction: options.instruction, mode: options.mode, rounds, maxConcurrent,
|
|
97
|
+
deadlineAt: Date.now() + seconds * 1000, generations: Object.fromEntries(members.map(v => [v.agentId, v.generation])),
|
|
98
|
+
state: "running" as const, issued: [] };
|
|
99
|
+
team.runs.push(run); this.files.save(team); return run;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
pause(id: string, owner: AgentOwner, reason = "主会话暂停") {
|
|
103
|
+
this.check(); this.agents.control(() => { const team = this.get(id, { kind: "coordinator", owner }); this.pauseRecord(team, reason); });
|
|
104
|
+
}
|
|
105
|
+
private pauseRecord(team: TeamRecord, reason: string) {
|
|
106
|
+
let changed = false;
|
|
107
|
+
for (const run of team.runs) if (run.state === "running") { run.state = "paused"; run.reason = reason; changed = true; }
|
|
108
|
+
if (changed) this.files.save(team);
|
|
109
|
+
}
|
|
110
|
+
pauseAll(owner: AgentOwner, reason: string) {
|
|
111
|
+
this.check(); this.agents.control(() => { for (const team of this.records(owner.sessionId)) this.pauseRecord(team, reason); });
|
|
112
|
+
}
|
|
113
|
+
recover(owner: AgentOwner) {
|
|
114
|
+
if (this.#recovered.has(owner.sessionId)) return;
|
|
115
|
+
this.pauseAll(owner, "主会话恢复:未自动续排旧批次"); this.#recovered.add(owner.sessionId);
|
|
116
|
+
}
|
|
117
|
+
pauseMember(agentId: string, reason: string) {
|
|
118
|
+
this.check(); this.agents.control(() => { for (const team of this.records()) if (team.members.some(m => m.agentId === agentId)) this.pauseRecord(team, reason); });
|
|
119
|
+
}
|
|
120
|
+
close(id: string, owner: AgentOwner, views: TeamAgent[]) {
|
|
121
|
+
this.check(); this.agents.control(() => {
|
|
122
|
+
const team = this.get(id, { kind: "coordinator", owner });
|
|
123
|
+
requireThat(!team.runs.some(r => r.state === "running") && !this.active(team, views).length, "先暂停团队并等待在途工作结束。");
|
|
124
|
+
if (!team.closed) { team.closed = true; this.files.save(team); }
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
post(id: string, actor: TeamActor, operationId: string, text: string, replyTo?: number) {
|
|
128
|
+
this.check(); requireThat(text.trim().length > 0 && text.length <= 24000 && operationId.length > 0, "消息须为 1–24000 字符。");
|
|
129
|
+
operationId = actor.kind === "member" ? "post:" + actor.agentId + ":" + actor.generation + ":" + operationId : "post:" + actor.kind + ":" + actor.owner.sessionId + ":" + operationId;
|
|
130
|
+
return this.agents.control(() => {
|
|
131
|
+
const team = this.get(id, actor);
|
|
132
|
+
const prior = team.messages.find(m => m.operationId === operationId);
|
|
133
|
+
if (prior) {
|
|
134
|
+
requireThat(prior.text === text && prior.replyTo === replyTo && prior.author.kind === actor.kind &&
|
|
135
|
+
(actor.kind !== "member" || prior.author.agentId === actor.agentId), "发布操作 ID 冲突。"); return prior;
|
|
136
|
+
}
|
|
137
|
+
requireThat(!team.closed, "团队已关闭。");
|
|
138
|
+
requireThat(replyTo === undefined || team.messages.some(m => m.seq === replyTo), "回复目标不存在。");
|
|
139
|
+
let runId: string | undefined;
|
|
140
|
+
if (actor.kind === "member") {
|
|
141
|
+
const run = team.runs.find(r => r.issued.some(q => q.requestId === actor.requestId && q.agentId === actor.agentId && q.generation === actor.generation));
|
|
142
|
+
const saved = this.agents.optional(actor.agentId, "status.json", status);
|
|
143
|
+
requireThat(run && saved?.generation === actor.generation && saved.cycle?.state === "running" && saved.cycle.source === "parent" &&
|
|
144
|
+
!!actor.requestId && saved.cycle.requestIds.includes(actor.requestId), "只能在自己的团队工作中发言。"); runId = run.runId;
|
|
145
|
+
}
|
|
146
|
+
const value: TeamMessage = { seq: team.messages.length + 1, operationId, text,
|
|
147
|
+
author: actor.kind === "member" ? { kind: "member", agentId: actor.agentId, name: this.agents.read(actor.agentId, "launch.json", launch).name }
|
|
148
|
+
: { kind: actor.kind, name: actor.kind === "user" ? "用户" : "主会话" },
|
|
149
|
+
...(replyTo === undefined ? {} : { replyTo }), ...(runId ? { runId } : {}) };
|
|
150
|
+
team.messages.push(value); this.files.save(team); return value;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
read(id: string, actor: TeamActor, page: { afterSeq?: number; seq?: number; offset?: number; limit?: number } = {}) {
|
|
154
|
+
const team = this.get(id, actor), limit = page.limit ?? 16000, after = page.afterSeq ?? 0, offset = page.offset ?? 0;
|
|
155
|
+
requireThat(Number.isInteger(limit) && limit >= 1 && limit <= 16000 && Number.isInteger(after) && after >= 0 && after <= team.messages.length &&
|
|
156
|
+
Number.isInteger(offset) && offset >= 0 && (page.seq !== undefined || offset === 0), "分页参数无效。");
|
|
157
|
+
const values = page.seq === undefined ? team.messages.filter(m => m.seq > after) : team.messages.filter(m => m.seq === page.seq);
|
|
158
|
+
requireThat(page.seq === undefined || values.length === 1, "消息不存在。");
|
|
159
|
+
let left = limit;
|
|
160
|
+
const messages = [];
|
|
161
|
+
for (const m of values) {
|
|
162
|
+
const at = page.seq === undefined ? 0 : offset;
|
|
163
|
+
requireThat(at <= m.text.length, "消息分页位置超出范围。");
|
|
164
|
+
const text = m.text.slice(at, at + left); left -= Math.max(1, text.length);
|
|
165
|
+
messages.push({ ...m, text, offset: at, totalLength: m.text.length, nextOffset: at + text.length < m.text.length ? at + text.length : null });
|
|
166
|
+
if (left <= 0) break;
|
|
167
|
+
}
|
|
168
|
+
const last = messages.at(-1)?.seq ?? after;
|
|
169
|
+
return { teamId: id, messages, lastSeq: team.messages.length, nextAfterSeq: page.seq === undefined && last < team.messages.length ? last : null };
|
|
170
|
+
}
|
|
171
|
+
status(id: string, owner: AgentOwner, views: TeamAgent[]) {
|
|
172
|
+
return this.summary(this.get(id, { kind: "coordinator", owner }), owner, views);
|
|
173
|
+
}
|
|
174
|
+
list(owner: AgentOwner, views: TeamAgent[]) { return this.records(owner.sessionId).map(t => this.summary(t, owner, views)); }
|
|
175
|
+
private summary(team: TeamRecord, owner: AgentOwner, views: TeamAgent[]) {
|
|
176
|
+
const identity = { teamId: team.teamId, name: team.name, closed: team.closed };
|
|
177
|
+
if (!this.owned(team, owner)) return { ...identity, owned: false as const };
|
|
178
|
+
const run = team.runs.at(-1), requests = this.requests(team, views), activeCount = requests.filter(q => q.active).length;
|
|
179
|
+
return { ...identity, brief: team.brief, owned: true as const,
|
|
180
|
+
members: team.members.map(m => { const v = views.find(v => v.agentId === m.agentId); return { ...m, name: v?.name ?? m.agentId, state: v?.state ?? "stopped" }; }),
|
|
181
|
+
lastSeq: team.messages.length, activeCount, unresolved: requests.filter(q => q.state !== "completed"),
|
|
182
|
+
...(run ? { run: { runId: run.runId, instruction: run.instruction, mode: run.mode, state: run.state, rounds: run.rounds,
|
|
183
|
+
maxConcurrent: run.maxConcurrent, deadlineAt: run.deadlineAt, scheduled: run.issued.length,
|
|
184
|
+
published: team.messages.filter(m => m.runId === run.runId && m.source).length, ...(run.reason ? { reason: run.reason } : {}) } } : {}) };
|
|
185
|
+
}
|
|
186
|
+
links() { return this.files.links(this.records()); }
|
|
187
|
+
/** The callback persists the existing child receipt while the shared control lock is held. */
|
|
188
|
+
claim(agentId: string, generation: string, requestId: string, commit: () => void): TeamLink | undefined {
|
|
189
|
+
this.check();
|
|
190
|
+
return this.agents.control(() => {
|
|
191
|
+
const link = this.links().get(requestId), run = this.agents.read(agentId, "launch.json", launch), ready = this.files.ready(agentId);
|
|
192
|
+
const saved = this.agents.optional(agentId, "status.json", status), input = this.agents.optional(agentId, "inbox/" + requestId + ".json", message);
|
|
193
|
+
if (!link || link.request.agentId !== agentId || link.request.generation !== generation || run.generation !== generation || run.stop ||
|
|
194
|
+
!ready?.ready || ready.generation !== generation || saved?.generation !== generation || saved.state !== "idle" ||
|
|
195
|
+
saved.accepted?.includes(requestId) || input?.generation !== generation || link.team.closed || link.run.state !== "running" || link.run.deadlineAt <= Date.now()) return;
|
|
196
|
+
for (const member of link.team.members) {
|
|
197
|
+
const memberRun = this.agents.read(member.agentId, "launch.json", launch), memberReady = this.files.ready(member.agentId);
|
|
198
|
+
if (memberRun.stop || memberRun.generation !== link.run.generations[member.agentId] || !memberReady?.ready || memberReady.generation !== memberRun.generation) return;
|
|
199
|
+
}
|
|
200
|
+
commit(); return link;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
advance(owner: AgentOwner, views: TeamAgent[], completions: AgentCompletion[]): TeamInput[] {
|
|
204
|
+
this.check();
|
|
205
|
+
return this.agents.control(() => {
|
|
206
|
+
const inputs: TeamInput[] = [], records = this.records(owner.sessionId), links = this.files.links(records);
|
|
207
|
+
for (const team of records) {
|
|
208
|
+
let changed = false;
|
|
209
|
+
for (const run of team.runs) {
|
|
210
|
+
for (const q of run.issued) {
|
|
211
|
+
const done = completions.find(v => v.agentId === q.agentId && v.cycle.generation === q.generation && v.cycle.requestIds.includes(q.requestId));
|
|
212
|
+
if (!done) continue;
|
|
213
|
+
if (done.cycle.state !== "completed" || done.cycle.source !== "parent" || done.cycle.requestIds.length !== 1 || !done.report.trim()) {
|
|
214
|
+
if (run.state === "running") { run.state = "paused"; run.reason = "成员失败或人工接管:" + q.agentId; changed = true; }
|
|
215
|
+
} else if (!team.messages.some(m => m.source?.requestId === q.requestId)) {
|
|
216
|
+
team.messages.push({ seq: team.messages.length + 1, operationId: "result:" + q.requestId,
|
|
217
|
+
author: { kind: "member", agentId: q.agentId, name: done.name }, text: done.report, runId: run.runId,
|
|
218
|
+
source: { requestId: q.requestId, cycleId: done.cycle.cycleId } }); changed = true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (run.state !== "running") continue;
|
|
222
|
+
const unavailable = team.members.find(m => {
|
|
223
|
+
const v = views.find(v => v.agentId === m.agentId), ready = this.files.ready(m.agentId);
|
|
224
|
+
return !v?.alive || !v.owned || v.generation !== run.generations[m.agentId] || !ready?.ready || ready.generation !== v.generation ||
|
|
225
|
+
["stopping", "stopped", "error"].includes(v.state) || (v.cycle?.state === "running" && v.cycle.source !== "parent");
|
|
226
|
+
});
|
|
227
|
+
if (!this.owned(team, owner) || unavailable || Date.now() >= run.deadlineAt) {
|
|
228
|
+
run.state = "paused"; run.reason = !this.owned(team, owner) ? "当前分支无团队归属" : unavailable ? "成员不可用:" + unavailable.agentId : "已到期限";
|
|
229
|
+
changed = true; continue;
|
|
230
|
+
}
|
|
231
|
+
const finished = new Set(team.messages.filter(m => m.runId === run.runId && m.source).map(m => m.source!.requestId));
|
|
232
|
+
const outstanding = run.issued.filter(q => !finished.has(q.requestId));
|
|
233
|
+
const round = Math.max(1, ...run.issued.map(q => q.round));
|
|
234
|
+
const thisRound = run.issued.filter(q => q.round === round);
|
|
235
|
+
const nextRound = thisRound.length === team.members.length && thisRound.every(q => finished.has(q.requestId)) ? round + 1 : round;
|
|
236
|
+
if (nextRound > run.rounds) { run.state = "finished"; changed = true; continue; }
|
|
237
|
+
let slots = run.maxConcurrent - outstanding.length;
|
|
238
|
+
for (const m of team.members) {
|
|
239
|
+
if (slots <= 0) break;
|
|
240
|
+
if (run.issued.some(q => q.round === nextRound && q.agentId === m.agentId)) continue;
|
|
241
|
+
// Waiting here does not create a second work queue. Unexpected private work pauses the batch.
|
|
242
|
+
const view = views.find(v => v.agentId === m.agentId);
|
|
243
|
+
if (view?.state === "busy" && view.cycle?.requestIds.some(id => run.issued.some(q => q.requestId === id))) break;
|
|
244
|
+
try { this.available(m.agentId, owner, views, links); }
|
|
245
|
+
catch (error) {
|
|
246
|
+
if ((error as NodeJS.ErrnoException).code !== "TEAM_NOT_READY") throw error;
|
|
247
|
+
run.state = "paused"; run.reason = String(error); changed = true; break;
|
|
248
|
+
}
|
|
249
|
+
const q = { requestId: randomUUID(), agentId: m.agentId, generation: run.generations[m.agentId]!, round: nextRound, throughSeq: team.messages.length };
|
|
250
|
+
run.issued.push(q); slots--; changed = true;
|
|
251
|
+
inputs.push({ ...q, teamId: team.teamId, runId: run.runId,
|
|
252
|
+
message: ["# 团队发言任务", "", "执行本次团队发言安排。最终答复将公开给全体成员;这不扩大原任务授权。", "",
|
|
253
|
+
"- `teamId`: `" + team.teamId + "`", "- `runId`: `" + run.runId + "`", "- 轮次:" + q.round,
|
|
254
|
+
"", "## 本批次要求", "", run.instruction].join("\n") });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (changed) this.files.save(team);
|
|
258
|
+
}
|
|
259
|
+
return inputs.filter(input => records.find(t => t.teamId === input.teamId)?.runs.find(r => r.runId === input.runId)?.state === "running");
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
notifications(owner: AgentOwner, views: TeamAgent[]) {
|
|
263
|
+
return this.records(owner.sessionId).filter(t => this.owned(t, owner) && t.members.every(m => views.some(v => v.agentId === m.agentId && v.owned)))
|
|
264
|
+
.flatMap(team => {
|
|
265
|
+
const requests = this.requests(team, views);
|
|
266
|
+
return team.runs.filter(r => r.state !== "running").map(run => {
|
|
267
|
+
const activeCount = requests.filter(q => q.runId === run.runId && q.active).length;
|
|
268
|
+
return { key: team.teamId + ":" + run.runId + ":" + run.state + (activeCount ? ":draining" : ":settled"),
|
|
269
|
+
teamId: team.teamId, name: team.name, runId: run.runId, state: run.state, activeCount,
|
|
270
|
+
...(run.reason ? { reason: run.reason } : {}) };
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync,
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { object } from "../common/json.js";
|
|
4
|
+
import { canonical, object } from "../common/json.js";
|
|
5
5
|
import { requireThat } from "../common/validation.js";
|
|
6
6
|
import { readJsonFile, writeJsonFile } from "./atomic-json.js";
|
|
7
7
|
import { fileId, privateDirectory } from "./file-access.js";
|
|
8
|
-
import type { AgentDescriptor, AgentLaunch, AgentMessage, AgentStatus, AgentCycle } from "../model/agent.js";
|
|
8
|
+
import type { AgentDescriptor, AgentLaunch, AgentMessage, AgentStatus, AgentCycle, AgentSessionRef } from "../model/agent.js";
|
|
9
9
|
|
|
10
10
|
const text = (v: unknown): v is string => typeof v === "string" && v.length > 0;
|
|
11
11
|
const number = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
|
|
@@ -14,6 +14,10 @@ export function descriptor(v: unknown): AgentDescriptor {
|
|
|
14
14
|
(v.parentLeaf === null || text(v.parentLeaf)) && number(v.createdAt) && object(v.options), "子会话身份无效。");
|
|
15
15
|
fileId(v.agentId as string); return v as unknown as AgentDescriptor;
|
|
16
16
|
}
|
|
17
|
+
export function sessionRef(v: unknown): AgentSessionRef {
|
|
18
|
+
requireThat(object(v) && (v.file === null || (text(v.file) && isAbsolute(v.file) && !v.file.includes("\0"))), "子会话引用无效。");
|
|
19
|
+
return v as unknown as AgentSessionRef;
|
|
20
|
+
}
|
|
17
21
|
export function launch(v: unknown): AgentLaunch {
|
|
18
22
|
requireThat(object(v) && text(v.generation) && text(v.name) && Number.isSafeInteger(v.sequence) && typeof v.stop === "boolean" && number(v.startedAt), "子会话启动状态无效。");
|
|
19
23
|
return v as unknown as AgentLaunch;
|
|
@@ -36,21 +40,30 @@ export function status(v: unknown): AgentStatus {
|
|
|
36
40
|
if (v.cycle) cycle(v.cycle); return v as unknown as AgentStatus;
|
|
37
41
|
}
|
|
38
42
|
export class AgentFiles {
|
|
43
|
+
#failure: unknown;
|
|
39
44
|
constructor(readonly root: string) {}
|
|
45
|
+
check() { if (this.#failure) throw this.#failure; }
|
|
46
|
+
fail(error: unknown) { this.#failure = error; }
|
|
40
47
|
directory(id: string) { return join(this.root, fileId(id)); }
|
|
41
48
|
read<T>(id: string, path: string, parse: (v: unknown) => T): T { return readJsonFile(this.directory(id), path, parse); }
|
|
42
49
|
optional<T>(id: string, path: string, parse: (v: unknown) => T): T | undefined {
|
|
43
50
|
try { return this.read(id, path, parse); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; throw error; }
|
|
44
51
|
}
|
|
45
52
|
write<T>(id: string, path: string, value: T, parse: (v: unknown) => T, create = false) {
|
|
46
|
-
|
|
53
|
+
this.check(); parse(value);
|
|
54
|
+
let published: boolean;
|
|
55
|
+
try { published = writeJsonFile(this.directory(id), path, value, parse, create); }
|
|
56
|
+
catch (error) { this.fail(error); throw error; }
|
|
57
|
+
requireThat(published, "子会话文件已存在。");
|
|
47
58
|
}
|
|
48
59
|
create(value: AgentDescriptor, run: AgentLaunch) {
|
|
60
|
+
this.check();
|
|
49
61
|
mkdirSync(this.root, { recursive: true, mode: 0o700 });
|
|
50
62
|
mkdirSync(this.directory(value.agentId), { mode: 0o700 });
|
|
51
63
|
for (const dir of ["inbox", "completions"]) privateDirectory(this.directory(value.agentId), dir);
|
|
52
64
|
this.write(value.agentId, "descriptor.json", value, descriptor, true);
|
|
53
65
|
this.write(value.agentId, "launch.json", run, launch, true);
|
|
66
|
+
this.write(value.agentId, "session-ref.json", { file: null }, sessionRef, true);
|
|
54
67
|
}
|
|
55
68
|
list() {
|
|
56
69
|
if (!existsSync(this.root)) return [];
|
|
@@ -66,19 +79,39 @@ export class AgentFiles {
|
|
|
66
79
|
return readdirSync(join(this.directory(id), "completions")).filter(n => n.endsWith(".json"))
|
|
67
80
|
.map(n => this.read(id, "completions/" + n, cycle)).sort((a, b) => (a.finishedAt ?? 0) - (b.finishedAt ?? 0));
|
|
68
81
|
}
|
|
69
|
-
session(id: string)
|
|
82
|
+
session(id: string): string | null {
|
|
83
|
+
const ref = this.optional(id, "session-ref.json", sessionRef);
|
|
84
|
+
requireThat(ref, "缺少原生会话引用;不导入旧记录,请重新 start。");
|
|
85
|
+
return ref.file;
|
|
86
|
+
}
|
|
87
|
+
/** The child fixes the native path before accepting input; never reset it to null. */
|
|
88
|
+
bindSession(id: string, file: string) {
|
|
89
|
+
const previous = this.session(id);
|
|
90
|
+
requireThat(previous === null || previous === file, "子会话文件引用不匹配。");
|
|
91
|
+
if (previous === null) this.write(id, "session-ref.json", { file }, sessionRef);
|
|
92
|
+
}
|
|
70
93
|
/** Short controller edits are serialized; an abandoned lock fails closed. */
|
|
71
94
|
control<T>(work: () => T): T {
|
|
95
|
+
this.check();
|
|
72
96
|
mkdirSync(this.root, { recursive: true, mode: 0o700 });
|
|
73
97
|
const path = join(this.root, ".control");
|
|
74
98
|
try { mkdirSync(path, { mode: 0o700 }); }
|
|
75
|
-
catch
|
|
99
|
+
catch (error) {
|
|
100
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
101
|
+
throw Object.assign(new Error("子会话控制正在进行;稍后重试。崩溃遗留的 .control 需停止相关 Pi 后手动移除。"), { code: "PBOT_CONTROL_BUSY" });
|
|
102
|
+
}
|
|
76
103
|
try { return work(); } finally { rmSync(path, { recursive: true }); }
|
|
77
104
|
}
|
|
78
|
-
enqueue(id: string, text: string, delivery: AgentMessage["delivery"], files: string[] = []) {
|
|
79
|
-
|
|
105
|
+
enqueue(id: string, text: string, delivery: AgentMessage["delivery"], files: string[] = [], requestId: string = randomUUID()) {
|
|
106
|
+
fileId(requestId);
|
|
107
|
+
const run = this.read(id, "launch.json", launch), previous = this.optional(id, "inbox/" + requestId + ".json", message);
|
|
108
|
+
if (previous) {
|
|
109
|
+
requireThat(previous.generation === run.generation && previous.message === text && previous.delivery === delivery && canonical(previous.files) === canonical(files), "请求 ID 已被不同输入使用。");
|
|
110
|
+
return previous;
|
|
111
|
+
}
|
|
112
|
+
run.sequence++;
|
|
80
113
|
this.write(id, "launch.json", run, launch);
|
|
81
|
-
const value: AgentMessage = { requestId
|
|
114
|
+
const value: AgentMessage = { requestId, generation: run.generation, sequence: run.sequence, message: text, delivery, files };
|
|
82
115
|
this.write(id, "inbox/" + value.requestId + ".json", value, message, true); return value;
|
|
83
116
|
}
|
|
84
117
|
}
|