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.
Files changed (44) hide show
  1. package/CONTEXT.md +6 -2
  2. package/README.md +9 -7
  3. package/README.zh-CN.md +9 -7
  4. package/docs/README.md +3 -1
  5. package/docs/agent-collaboration.md +98 -40
  6. package/docs/file-storage.md +25 -4
  7. package/docs/release.md +2 -2
  8. package/docs/subagent-upgrade-proposal.md +357 -0
  9. package/docs/technical-design.md +23 -4
  10. package/docs/validation.md +35 -5
  11. package/npm-shrinkwrap.json +2253 -0
  12. package/package.json +5 -4
  13. package/resources/agents/coordinator.md +8 -1
  14. package/resources/agents/general-purpose.md +10 -4
  15. package/resources/prompts/tool-guidelines.md +2 -2
  16. package/resources/prompts/workspace.md +2 -2
  17. package/resources/references/compute.md +3 -3
  18. package/resources/references/task-conductor.md +4 -4
  19. package/resources/skills/project-maintenance/SKILL.md +1 -1
  20. package/resources/skills/provider-connections/SKILL.md +2 -2
  21. package/src/common/markdown.ts +26 -0
  22. package/src/extension/agent-events.ts +116 -14
  23. package/src/extension/agents-command.ts +15 -17
  24. package/src/extension/command.ts +5 -3
  25. package/src/extension/lifecycle.ts +7 -2
  26. package/src/extension/subagent-entry.ts +144 -31
  27. package/src/extension/teams-command.ts +60 -0
  28. package/src/integrations/pi/agent-context.ts +1 -1
  29. package/src/integrations/pi/agent-input.ts +19 -0
  30. package/src/integrations/pi/pi-agents.ts +22 -9
  31. package/src/integrations/pi/team-context.ts +78 -0
  32. package/src/model/agent.ts +2 -0
  33. package/src/model/team.ts +29 -0
  34. package/src/pbot.ts +5 -3
  35. package/src/services/agents/agent-service.ts +91 -21
  36. package/src/services/agents/team-service.ts +274 -0
  37. package/src/storage/agent-files.ts +43 -10
  38. package/src/storage/team-files.ts +93 -0
  39. package/src/tools/agents/contract.ts +8 -6
  40. package/src/tools/agents/tool.ts +10 -9
  41. package/src/tools/register.ts +6 -3
  42. package/src/tools/result.ts +5 -0
  43. package/src/tools/teams/contract.ts +67 -0
  44. 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 type { AgentOwner, AgentOptions, AgentView, AgentDescriptor, AgentCycle, AgentCompletion } from "../../model/agent.js";
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
- launch(value: AgentDescriptor, directory: string, generation: string): void;
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 | null): 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) { this.files = new AgentFiles(directory); }
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
- list(owner: AgentOwner): AgentView[] {
30
- this.check(); return this.files.list().filter(d => d.workspace === this.root && d.parentSessionId === owner.sessionId).map(d => this.view(d, owner));
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
- private view(d: AgentDescriptor, owner: AgentOwner): AgentView {
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 request = this.files.enqueue(agentId, [options.message, ...(options.prompts ?? [])].filter(Boolean).join("\n\n"), "steer", files);
56
- try { this.host.launch(d, this.files.directory(agentId), run.generation); }
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 (!values.some(c => c.generation === view.generation) && ["stopped", "error"].includes(view.state)) {
110
- const run = this.files.read(id, "launch.json", launch);
111
- values.push({ cycleId: run.generation, generation: run.generation, startedAt: run.startedAt, finishedAt: run.startedAt,
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.host.report(this.files.session(id), c.leaf) : "";
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.host.report(this.files.session(v.agentId), c.leaf) })));
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.files.list().filter(d => d.workspace === this.root && d.parentSessionId === owner.sessionId)) {
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
- for (const id of ids) this.requestStop(id);
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, realpathSync, rmSync } from "node:fs";
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
- requireThat(writeJsonFile(this.directory(id), path, value, parse, create), "子会话文件已存在。");
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) { return join(realpathSync(this.directory(id)), "session.jsonl"); }
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 { throw new Error("子会话控制正在进行;稍后重试。崩溃遗留的 .control 需停止相关 Pi 后手动移除。"); }
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
- const run = this.read(id, "launch.json", launch); run.sequence++;
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: randomUUID(), generation: run.generation, sequence: run.sequence, message: text, delivery, files };
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
  }