@cuylabs/agent-core 0.9.0 → 0.11.0
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/README.md +33 -17
- package/dist/chunk-2O4MCSQS.js +780 -0
- package/dist/chunk-2TTOLHBT.js +198 -0
- package/dist/chunk-5FMSGQVX.js +281 -0
- package/dist/chunk-5NVVNXPQ.js +288 -0
- package/dist/{chunk-EKR6PKXU.js → chunk-6HZBHFOL.js} +3 -3
- package/dist/chunk-CJI7PVS2.js +58 -0
- package/dist/{chunk-WKHDSSXG.js → chunk-CMYN2RCB.js} +146 -46
- package/dist/chunk-FII65CN7.js +117 -0
- package/dist/{chunk-UHCJEM2E.js → chunk-ICZ66572.js} +13 -6
- package/dist/chunk-KYLPMBHD.js +316 -0
- package/dist/chunk-MXAP4UG6.js +2956 -0
- package/dist/{chunk-4QFNWPIF.js → chunk-N3VX7FEE.js} +35 -2
- package/dist/{chunk-MAZ5DY5B.js → chunk-NDZWXCBZ.js} +213 -78
- package/dist/{chunk-MHKK374K.js → chunk-Q742PSH3.js} +11 -27
- package/dist/{chunk-WGZAPU6N.js → chunk-QAL3OMI3.js} +15 -1
- package/dist/{chunk-UDCZ673N.js → chunk-RN6WZEUF.js} +27 -23
- package/dist/{chunk-ZXAKHMWH.js → chunk-ROTGCYDW.js} +22 -84
- package/dist/chunk-SPBFQXOT.js +0 -0
- package/dist/chunk-SSFBF3US.js +602 -0
- package/dist/chunk-SZ2XBPTW.js +8 -0
- package/dist/chunk-T4UIX5D7.js +115 -0
- package/dist/{chunk-IYWQOJMQ.js → chunk-TIHPYVAJ.js} +34 -34
- package/dist/{chunk-RKEW5WXI.js → chunk-TOTDGK3P.js} +1 -1
- package/dist/chunk-V4RFNEET.js +563 -0
- package/dist/chunk-VOUEJSW6.js +0 -0
- package/dist/{chunk-J4QDGZIA.js → chunk-WBPOZ7CL.js} +659 -275
- package/dist/chunk-X4VN4GIJ.js +185 -0
- package/dist/dispatch/index.d.ts +93 -0
- package/dist/dispatch/index.js +37 -0
- package/dist/events/index.d.ts +93 -0
- package/dist/events/index.js +6 -0
- package/dist/{runtime → execution}/index.d.ts +120 -35
- package/dist/{runtime → execution}/index.js +17 -11
- package/dist/index.d.ts +489 -115
- package/dist/index.js +1665 -462
- package/dist/inference/errors/index.js +1 -1
- package/dist/inference/index.d.ts +13 -21
- package/dist/inference/index.js +15 -12
- package/dist/instance-DzPiv6EK.d.ts +5723 -0
- package/dist/logger/index.d.ts +50 -0
- package/dist/logger/index.js +11 -0
- package/dist/mcp/index.d.ts +5 -9
- package/dist/mcp/index.js +2 -3
- package/dist/middleware/index.d.ts +10 -150
- package/dist/middleware/index.js +10 -2
- package/dist/model-messages-CJfwfzGe.d.ts +13 -0
- package/dist/models/index.d.ts +5 -2
- package/dist/models/index.js +2 -1
- package/dist/models/reasoning/index.js +2 -1
- package/dist/plugin/index.d.ts +55 -11
- package/dist/plugin/index.js +1 -1
- package/dist/profiles/index.d.ts +55 -0
- package/dist/{presets → profiles}/index.js +10 -10
- package/dist/prompt/index.d.ts +8 -13
- package/dist/safety/index.d.ts +109 -14
- package/dist/safety/index.js +59 -3
- package/dist/sandbox/index.d.ts +81 -0
- package/dist/sandbox/index.js +1 -0
- package/dist/skill/index.d.ts +10 -8
- package/dist/skill/index.js +2 -2
- package/dist/storage/index.d.ts +12 -4
- package/dist/storage/index.js +1 -1
- package/dist/subagents/index.d.ts +177 -0
- package/dist/subagents/index.js +78 -0
- package/dist/team/index.d.ts +544 -0
- package/dist/team/index.js +41 -0
- package/dist/tool/host/index.d.ts +41 -0
- package/dist/tool/host/index.js +10 -0
- package/dist/tool/index.d.ts +111 -21
- package/dist/tool/index.js +20 -13
- package/dist/{types-VQgymC1N.d.ts → types-Bj_J8u_W.d.ts} +44 -64
- package/dist/{types-CHiPh8U2.d.ts → types-C_LCeYNg.d.ts} +7 -7
- package/dist/types-RSCv7nQ4.d.ts +59 -0
- package/package.json +46 -47
- package/dist/builder-BgZ_j4Vs.d.ts +0 -35
- package/dist/chunk-5ARZJWD2.js +0 -259
- package/dist/chunk-DXFBQMXP.js +0 -53
- package/dist/chunk-H3FUYU52.js +0 -81
- package/dist/chunk-JLXG2SH7.js +0 -905
- package/dist/chunk-N7P4PN3O.js +0 -84
- package/dist/chunk-OFDKHNCX.js +0 -727
- package/dist/chunk-SDSBEQXG.js +0 -157
- package/dist/chunk-VEKUXUVF.js +0 -41
- package/dist/chunk-VNQBHPCT.js +0 -398
- package/dist/chunk-WWYYNWEW.js +0 -259
- package/dist/context/index.d.ts +0 -259
- package/dist/context/index.js +0 -26
- package/dist/events-CE72w8W4.d.ts +0 -149
- package/dist/host/index.d.ts +0 -45
- package/dist/host/index.js +0 -8
- package/dist/index-DQuTZ8xL.d.ts +0 -1335
- package/dist/messages-BYWGn8TY.d.ts +0 -110
- package/dist/presets/index.d.ts +0 -53
- package/dist/registry-DwYqsQkX.d.ts +0 -164
- package/dist/runner-CI-XeR16.d.ts +0 -91
- package/dist/scope/index.d.ts +0 -10
- package/dist/scope/index.js +0 -14
- package/dist/session-manager-KbYt2WUh.d.ts +0 -282
- package/dist/signal/index.d.ts +0 -28
- package/dist/signal/index.js +0 -6
- package/dist/sub-agent/index.d.ts +0 -24
- package/dist/sub-agent/index.js +0 -32
- package/dist/tool-CZWN3KbO.d.ts +0 -141
- package/dist/tool-DkhSCV2Y.d.ts +0 -145
- package/dist/tracker-DClqYqTj.d.ts +0 -96
- package/dist/tracking/index.d.ts +0 -111
- package/dist/tracking/index.js +0 -20
- package/dist/types-BfNpU8NS.d.ts +0 -270
- package/dist/types-BlOKk-Bb.d.ts +0 -330
- package/dist/types-BlZwmnuW.d.ts +0 -50
- package/dist/types-CQL-SvTn.d.ts +0 -29
- package/dist/types-CWm-7rvB.d.ts +0 -55
- package/dist/types-DTSkxakL.d.ts +0 -651
- package/dist/types-DmDwi2zI.d.ts +0 -339
- package/dist/types-YuWV4ag7.d.ts +0 -72
|
@@ -0,0 +1,2956 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sleep
|
|
3
|
+
} from "./chunk-SZ2XBPTW.js";
|
|
4
|
+
import {
|
|
5
|
+
Tool
|
|
6
|
+
} from "./chunk-Q742PSH3.js";
|
|
7
|
+
import {
|
|
8
|
+
extractApprovalPatterns,
|
|
9
|
+
matchApprovalPattern,
|
|
10
|
+
resolveCapability
|
|
11
|
+
} from "./chunk-FII65CN7.js";
|
|
12
|
+
|
|
13
|
+
// src/team/types.ts
|
|
14
|
+
var TERMINAL_STATUSES = [
|
|
15
|
+
"completed",
|
|
16
|
+
"aborted",
|
|
17
|
+
"failed",
|
|
18
|
+
"cancelled"
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// src/team/task-board.ts
|
|
22
|
+
var TASK_CONFLICT_MAX_RETRIES = 4;
|
|
23
|
+
var TaskConflictError = class extends Error {
|
|
24
|
+
constructor(message, taskId) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.taskId = taskId;
|
|
27
|
+
this.name = "TaskConflictError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function clone(value) {
|
|
31
|
+
return structuredClone(value);
|
|
32
|
+
}
|
|
33
|
+
function now() {
|
|
34
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
35
|
+
}
|
|
36
|
+
function requireNonEmpty(value, label) {
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
if (!trimmed) throw new Error(`${label} must not be empty`);
|
|
39
|
+
return trimmed;
|
|
40
|
+
}
|
|
41
|
+
function normalizeDeps(taskId, deps) {
|
|
42
|
+
if (!deps || deps.length === 0) return void 0;
|
|
43
|
+
const unique = [...new Set(deps.map((d) => requireNonEmpty(d, "dependency ID")))];
|
|
44
|
+
if (unique.includes(taskId)) throw new Error(`Task "${taskId}" cannot depend on itself`);
|
|
45
|
+
return unique;
|
|
46
|
+
}
|
|
47
|
+
function isTerminal(status) {
|
|
48
|
+
return status === "completed" || status === "aborted" || status === "failed" || status === "cancelled";
|
|
49
|
+
}
|
|
50
|
+
function isDependencyFailure(status) {
|
|
51
|
+
return status === "failed" || status === "cancelled";
|
|
52
|
+
}
|
|
53
|
+
function resolveDependencyStatus(task, lookup) {
|
|
54
|
+
const deps = task.dependsOn ?? [];
|
|
55
|
+
if (deps.length === 0) {
|
|
56
|
+
return {
|
|
57
|
+
status: task.status === "blocked" ? "pending" : task.status,
|
|
58
|
+
failedDeps: [],
|
|
59
|
+
blockedBy: []
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const failedDeps = [];
|
|
63
|
+
const blockedBy = [];
|
|
64
|
+
for (const depId of deps) {
|
|
65
|
+
const dep = lookup.get(depId);
|
|
66
|
+
if (!dep) {
|
|
67
|
+
blockedBy.push(depId);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (dep.status === "completed") continue;
|
|
71
|
+
if (isDependencyFailure(dep.status)) {
|
|
72
|
+
failedDeps.push(depId);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
blockedBy.push(depId);
|
|
76
|
+
}
|
|
77
|
+
if (failedDeps.length > 0) {
|
|
78
|
+
return { status: "cancelled", failedDeps, blockedBy };
|
|
79
|
+
}
|
|
80
|
+
if (blockedBy.length > 0) {
|
|
81
|
+
return { status: "blocked", failedDeps, blockedBy };
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
status: task.status === "blocked" ? "pending" : task.status,
|
|
85
|
+
failedDeps: [],
|
|
86
|
+
blockedBy: []
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
var InMemoryTaskBoardStore = class {
|
|
90
|
+
members = /* @__PURE__ */ new Map();
|
|
91
|
+
tasks = /* @__PURE__ */ new Map();
|
|
92
|
+
async listMembers() {
|
|
93
|
+
return [...this.members.values()].map(clone).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
94
|
+
}
|
|
95
|
+
async getMember(memberId) {
|
|
96
|
+
const m = this.members.get(memberId);
|
|
97
|
+
return m ? clone(m) : void 0;
|
|
98
|
+
}
|
|
99
|
+
async putMember(member) {
|
|
100
|
+
const copy = clone(member);
|
|
101
|
+
this.members.set(copy.id, copy);
|
|
102
|
+
return clone(copy);
|
|
103
|
+
}
|
|
104
|
+
async listTasks(filter) {
|
|
105
|
+
let tasks = [...this.tasks.values()];
|
|
106
|
+
if (filter?.memberId) {
|
|
107
|
+
tasks = tasks.filter((t) => t.memberId === filter.memberId);
|
|
108
|
+
}
|
|
109
|
+
if (filter?.status) {
|
|
110
|
+
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
111
|
+
tasks = tasks.filter((t) => statuses.includes(t.status));
|
|
112
|
+
}
|
|
113
|
+
const sorted = tasks.map(clone).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
114
|
+
if (filter?.limit !== void 0) {
|
|
115
|
+
return sorted.slice(0, Math.max(0, Math.floor(filter.limit)));
|
|
116
|
+
}
|
|
117
|
+
return sorted;
|
|
118
|
+
}
|
|
119
|
+
async getTask(taskId) {
|
|
120
|
+
const t = this.tasks.get(taskId);
|
|
121
|
+
return t ? clone(t) : void 0;
|
|
122
|
+
}
|
|
123
|
+
async putTask(task) {
|
|
124
|
+
const copy = clone(task);
|
|
125
|
+
this.tasks.set(copy.id, copy);
|
|
126
|
+
return clone(copy);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var TaskBoard = class {
|
|
130
|
+
constructor(store) {
|
|
131
|
+
this.store = store;
|
|
132
|
+
}
|
|
133
|
+
async start() {
|
|
134
|
+
await this.store.start?.();
|
|
135
|
+
}
|
|
136
|
+
async stop() {
|
|
137
|
+
await this.store.stop?.();
|
|
138
|
+
}
|
|
139
|
+
// ── Read ───────────────────────────────────────────────────────────
|
|
140
|
+
async listMembers() {
|
|
141
|
+
return this.store.listMembers();
|
|
142
|
+
}
|
|
143
|
+
async getMember(id) {
|
|
144
|
+
return this.store.getMember(id);
|
|
145
|
+
}
|
|
146
|
+
async listTasks(filter) {
|
|
147
|
+
return this.store.listTasks(filter);
|
|
148
|
+
}
|
|
149
|
+
async getTask(id) {
|
|
150
|
+
return this.store.getTask(id);
|
|
151
|
+
}
|
|
152
|
+
async snapshot() {
|
|
153
|
+
const [members, tasks] = await Promise.all([
|
|
154
|
+
this.store.listMembers(),
|
|
155
|
+
this.store.listTasks()
|
|
156
|
+
]);
|
|
157
|
+
return { members, tasks };
|
|
158
|
+
}
|
|
159
|
+
// ── Member mutations ───────────────────────────────────────────────
|
|
160
|
+
async putMember(member) {
|
|
161
|
+
return this.retryOnConflict(() => this.store.putMember(member));
|
|
162
|
+
}
|
|
163
|
+
// ── Task lifecycle ─────────────────────────────────────────────────
|
|
164
|
+
/**
|
|
165
|
+
* Create a new task. Its initial status is computed from dependencies:
|
|
166
|
+
* `"pending"` if all deps are met, `"blocked"` otherwise, or
|
|
167
|
+
* `"cancelled"` if a dependency already failed.
|
|
168
|
+
*/
|
|
169
|
+
async createTask(input) {
|
|
170
|
+
return this.retryOnConflict(async () => {
|
|
171
|
+
const id = requireNonEmpty(input.id, "task ID");
|
|
172
|
+
const existing = await this.store.getTask(id);
|
|
173
|
+
if (existing) throw new Error(`Task already exists: ${id}`);
|
|
174
|
+
const ts = now();
|
|
175
|
+
const task = {
|
|
176
|
+
id,
|
|
177
|
+
memberId: input.memberId?.trim() || void 0,
|
|
178
|
+
title: requireNonEmpty(input.title, "title"),
|
|
179
|
+
prompt: requireNonEmpty(input.prompt, "prompt"),
|
|
180
|
+
status: "pending",
|
|
181
|
+
dependsOn: normalizeDeps(id, input.dependsOn),
|
|
182
|
+
createdAt: ts,
|
|
183
|
+
updatedAt: ts
|
|
184
|
+
};
|
|
185
|
+
const allTasks = await this.store.listTasks();
|
|
186
|
+
const lookup = new Map(allTasks.map((t) => [t.id, t]));
|
|
187
|
+
const resolved = resolveDependencyStatus(task, lookup);
|
|
188
|
+
const initial = {
|
|
189
|
+
...task,
|
|
190
|
+
status: resolved.status,
|
|
191
|
+
error: resolved.failedDeps.length > 0 ? `Dependency failed: ${resolved.failedDeps.join(", ")}` : void 0
|
|
192
|
+
};
|
|
193
|
+
const stored = await this.store.putTask(initial);
|
|
194
|
+
return {
|
|
195
|
+
task: stored,
|
|
196
|
+
transitions: [{ task: stored, reason: "created" }]
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Claim a pending task for a member. Moves status to `"claimed"`.
|
|
202
|
+
*/
|
|
203
|
+
async claimTask(taskId, memberId) {
|
|
204
|
+
return this.retryOnConflict(async () => {
|
|
205
|
+
const id = requireNonEmpty(memberId, "member ID");
|
|
206
|
+
const task = await this.requireTask(taskId);
|
|
207
|
+
if (task.status !== "pending") {
|
|
208
|
+
throw new Error(`Task "${taskId}" is not claimable (status: ${task.status})`);
|
|
209
|
+
}
|
|
210
|
+
if (task.memberId && task.memberId !== id) {
|
|
211
|
+
throw new Error(`Task "${taskId}" is assigned to ${task.memberId}, not ${id}`);
|
|
212
|
+
}
|
|
213
|
+
const ts = now();
|
|
214
|
+
return this.store.putTask({
|
|
215
|
+
...task,
|
|
216
|
+
memberId: id,
|
|
217
|
+
status: "claimed",
|
|
218
|
+
claimedAt: ts,
|
|
219
|
+
updatedAt: ts
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Claim the next available pending task for a member.
|
|
225
|
+
* Prefers tasks already assigned to this member, then unassigned tasks.
|
|
226
|
+
*/
|
|
227
|
+
async claimNextTask(memberId) {
|
|
228
|
+
const id = requireNonEmpty(memberId, "member ID");
|
|
229
|
+
const pending = await this.store.listTasks({ status: "pending" });
|
|
230
|
+
const next = pending.find((t) => t.memberId === id) ?? pending.find((t) => !t.memberId);
|
|
231
|
+
if (!next) return void 0;
|
|
232
|
+
return this.claimTask(next.id, id);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Mark a claimed task as running with a run reference.
|
|
236
|
+
*/
|
|
237
|
+
async startTask(taskId, runId) {
|
|
238
|
+
return this.retryOnConflict(async () => {
|
|
239
|
+
const task = await this.requireTask(taskId);
|
|
240
|
+
if (task.status !== "claimed") {
|
|
241
|
+
throw new Error(`Task "${taskId}" must be claimed before starting (status: ${task.status})`);
|
|
242
|
+
}
|
|
243
|
+
return this.store.putTask({
|
|
244
|
+
...task,
|
|
245
|
+
status: "running",
|
|
246
|
+
runId,
|
|
247
|
+
updatedAt: now()
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Complete a running task with a result.
|
|
253
|
+
* Cascades: blocked dependents may become pending or cancelled.
|
|
254
|
+
*/
|
|
255
|
+
async completeTask(taskId, result) {
|
|
256
|
+
return this.retryOnConflict(async () => {
|
|
257
|
+
const task = await this.requireTask(taskId);
|
|
258
|
+
const completed = {
|
|
259
|
+
...task,
|
|
260
|
+
status: "completed",
|
|
261
|
+
result,
|
|
262
|
+
error: void 0,
|
|
263
|
+
updatedAt: now()
|
|
264
|
+
};
|
|
265
|
+
const stored = await this.store.putTask(completed);
|
|
266
|
+
const cascaded = await this.refreshDependents();
|
|
267
|
+
return {
|
|
268
|
+
task: stored,
|
|
269
|
+
transitions: [{ task: stored, reason: "completed", previous: task.status }, ...cascaded]
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Fail a running task.
|
|
275
|
+
* Cascades: blocked dependents may be cancelled.
|
|
276
|
+
*/
|
|
277
|
+
async failTask(taskId, error) {
|
|
278
|
+
return this.retryOnConflict(async () => {
|
|
279
|
+
const task = await this.requireTask(taskId);
|
|
280
|
+
const failed = {
|
|
281
|
+
...task,
|
|
282
|
+
status: "failed",
|
|
283
|
+
result: void 0,
|
|
284
|
+
error: requireNonEmpty(error, "error"),
|
|
285
|
+
updatedAt: now()
|
|
286
|
+
};
|
|
287
|
+
const stored = await this.store.putTask(failed);
|
|
288
|
+
const cascaded = await this.refreshDependents();
|
|
289
|
+
return {
|
|
290
|
+
task: stored,
|
|
291
|
+
transitions: [{ task: stored, reason: "failed", previous: task.status }, ...cascaded]
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Abort a claimed or running task without cascading dependency failure.
|
|
297
|
+
*
|
|
298
|
+
* Downstream tasks remain blocked so the host can retry or replace the
|
|
299
|
+
* aborted prerequisite explicitly.
|
|
300
|
+
*/
|
|
301
|
+
async abortTask(taskId, reason) {
|
|
302
|
+
return this.retryOnConflict(async () => {
|
|
303
|
+
const task = await this.requireTask(taskId);
|
|
304
|
+
if (isTerminal(task.status)) {
|
|
305
|
+
throw new Error(`Task "${taskId}" is already terminal (status: ${task.status})`);
|
|
306
|
+
}
|
|
307
|
+
const aborted = {
|
|
308
|
+
...task,
|
|
309
|
+
status: "aborted",
|
|
310
|
+
result: void 0,
|
|
311
|
+
error: reason?.trim() || task.error,
|
|
312
|
+
updatedAt: now()
|
|
313
|
+
};
|
|
314
|
+
const stored = await this.store.putTask(aborted);
|
|
315
|
+
return {
|
|
316
|
+
task: stored,
|
|
317
|
+
transitions: [{ task: stored, reason: "aborted", previous: task.status }]
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Cancel a task (at any non-terminal status).
|
|
323
|
+
* Cascades: blocked dependents may be cancelled.
|
|
324
|
+
*/
|
|
325
|
+
async cancelTask(taskId, reason) {
|
|
326
|
+
return this.retryOnConflict(async () => {
|
|
327
|
+
const task = await this.requireTask(taskId);
|
|
328
|
+
if (isTerminal(task.status)) {
|
|
329
|
+
throw new Error(`Task "${taskId}" is already terminal (status: ${task.status})`);
|
|
330
|
+
}
|
|
331
|
+
const cancelled = {
|
|
332
|
+
...task,
|
|
333
|
+
status: "cancelled",
|
|
334
|
+
result: void 0,
|
|
335
|
+
error: reason?.trim() || task.error,
|
|
336
|
+
updatedAt: now()
|
|
337
|
+
};
|
|
338
|
+
const stored = await this.store.putTask(cancelled);
|
|
339
|
+
const cascaded = await this.refreshDependents();
|
|
340
|
+
return {
|
|
341
|
+
task: stored,
|
|
342
|
+
transitions: [{ task: stored, reason: "cancelled", previous: task.status }, ...cascaded]
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Re-evaluate all blocked tasks against current dependency state.
|
|
348
|
+
* Called automatically after complete/fail/cancel, but can also
|
|
349
|
+
* be called manually for batch operations.
|
|
350
|
+
*/
|
|
351
|
+
async refreshDependents() {
|
|
352
|
+
return this.retryOnConflict(async () => {
|
|
353
|
+
const allTasks = await this.store.listTasks();
|
|
354
|
+
const lookup = new Map(allTasks.map((t) => [t.id, t]));
|
|
355
|
+
const transitions = [];
|
|
356
|
+
let changed = true;
|
|
357
|
+
while (changed) {
|
|
358
|
+
changed = false;
|
|
359
|
+
for (const task of lookup.values()) {
|
|
360
|
+
if (task.status !== "blocked") continue;
|
|
361
|
+
const resolved = resolveDependencyStatus(task, lookup);
|
|
362
|
+
if (resolved.status === task.status) continue;
|
|
363
|
+
const updated = {
|
|
364
|
+
...task,
|
|
365
|
+
status: resolved.status,
|
|
366
|
+
updatedAt: now(),
|
|
367
|
+
error: resolved.failedDeps.length > 0 ? `Dependency failed: ${resolved.failedDeps.join(", ")}` : void 0
|
|
368
|
+
};
|
|
369
|
+
const stored = await this.store.putTask(updated);
|
|
370
|
+
lookup.set(stored.id, stored);
|
|
371
|
+
changed = true;
|
|
372
|
+
transitions.push({
|
|
373
|
+
task: stored,
|
|
374
|
+
reason: resolved.failedDeps.length > 0 ? "blocked-dependency-failed" : "unblocked",
|
|
375
|
+
previous: task.status
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return transitions;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
// ── Internal ───────────────────────────────────────────────────────
|
|
383
|
+
async requireTask(taskId) {
|
|
384
|
+
const task = await this.store.getTask(taskId);
|
|
385
|
+
if (!task) throw new Error(`Unknown task: ${taskId}`);
|
|
386
|
+
return task;
|
|
387
|
+
}
|
|
388
|
+
async retryOnConflict(op) {
|
|
389
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
390
|
+
try {
|
|
391
|
+
return await op();
|
|
392
|
+
} catch (error) {
|
|
393
|
+
if (error instanceof TaskConflictError && attempt + 1 < TASK_CONFLICT_MAX_RETRIES) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// src/team/mailbox.ts
|
|
403
|
+
import { randomUUID } from "crypto";
|
|
404
|
+
function clone2(value) {
|
|
405
|
+
return structuredClone(value);
|
|
406
|
+
}
|
|
407
|
+
function requireNonEmpty2(value, label) {
|
|
408
|
+
const trimmed = value.trim();
|
|
409
|
+
if (!trimmed) throw new Error(`${label} must not be empty`);
|
|
410
|
+
return trimmed;
|
|
411
|
+
}
|
|
412
|
+
function matchesKind(msg, kind) {
|
|
413
|
+
if (!kind) return true;
|
|
414
|
+
const list = Array.isArray(kind) ? kind : [kind];
|
|
415
|
+
return list.includes(msg.kind);
|
|
416
|
+
}
|
|
417
|
+
var InMemoryMailboxStore = class {
|
|
418
|
+
messages = [];
|
|
419
|
+
async append(message) {
|
|
420
|
+
const copy = clone2(message);
|
|
421
|
+
this.messages.push(copy);
|
|
422
|
+
return clone2(copy);
|
|
423
|
+
}
|
|
424
|
+
async list(filter) {
|
|
425
|
+
let msgs = this.messages;
|
|
426
|
+
if (filter?.teamId) msgs = msgs.filter((m) => m.teamId === filter.teamId);
|
|
427
|
+
if (filter?.from) msgs = msgs.filter((m) => m.from === filter.from);
|
|
428
|
+
if (filter?.to) msgs = msgs.filter((m) => m.to === filter.to);
|
|
429
|
+
if (filter?.taskId) msgs = msgs.filter((m) => m.taskId === filter.taskId);
|
|
430
|
+
if (filter?.kind) msgs = msgs.filter((m) => matchesKind(m, filter.kind));
|
|
431
|
+
const sorted = msgs.map(clone2).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
432
|
+
if (filter?.limit !== void 0) {
|
|
433
|
+
const n = Math.max(0, Math.floor(filter.limit));
|
|
434
|
+
return sorted.slice(Math.max(0, sorted.length - n));
|
|
435
|
+
}
|
|
436
|
+
return sorted;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
var Mailbox = class {
|
|
440
|
+
store;
|
|
441
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
442
|
+
createId;
|
|
443
|
+
constructor(options) {
|
|
444
|
+
this.store = options?.store ?? new InMemoryMailboxStore();
|
|
445
|
+
this.createId = options?.createId ?? (() => randomUUID());
|
|
446
|
+
}
|
|
447
|
+
async start() {
|
|
448
|
+
await this.store.start?.();
|
|
449
|
+
}
|
|
450
|
+
async stop() {
|
|
451
|
+
await this.store.stop?.();
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Send a message. Notifies subscribers after persistence.
|
|
455
|
+
*/
|
|
456
|
+
async send(input) {
|
|
457
|
+
const to = input.to?.trim() || void 0;
|
|
458
|
+
const message = {
|
|
459
|
+
id: input.id?.trim() || this.createId(),
|
|
460
|
+
teamId: requireNonEmpty2(input.teamId, "teamId"),
|
|
461
|
+
from: requireNonEmpty2(input.from, "from"),
|
|
462
|
+
to,
|
|
463
|
+
kind: input.kind ?? (to ? "direct" : "broadcast"),
|
|
464
|
+
body: requireNonEmpty2(input.body, "body"),
|
|
465
|
+
payload: input.payload ? clone2(input.payload) : void 0,
|
|
466
|
+
taskId: input.taskId?.trim() || void 0,
|
|
467
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
468
|
+
metadata: input.metadata ? { ...input.metadata } : void 0
|
|
469
|
+
};
|
|
470
|
+
const stored = await this.store.append(message);
|
|
471
|
+
if (this.subscribers.size > 0) {
|
|
472
|
+
const delivered = clone2(stored);
|
|
473
|
+
await Promise.allSettled(
|
|
474
|
+
[...this.subscribers].map((sub) => sub(clone2(delivered)))
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return clone2(stored);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* List messages matching the filter.
|
|
481
|
+
*/
|
|
482
|
+
async list(filter) {
|
|
483
|
+
return this.store.list(filter);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Subscribe to new messages. Returns an unsubscribe function.
|
|
487
|
+
*/
|
|
488
|
+
subscribe(subscriber) {
|
|
489
|
+
this.subscribers.add(subscriber);
|
|
490
|
+
return () => {
|
|
491
|
+
this.subscribers.delete(subscriber);
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// src/team/coordinator/coordinator.ts
|
|
497
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
498
|
+
|
|
499
|
+
// src/team/permissions.ts
|
|
500
|
+
function matchesRule(rule, memberId, toolName, args, ctx) {
|
|
501
|
+
if (!matchApprovalPattern(rule.tool, toolName)) return false;
|
|
502
|
+
if (rule.members && !rule.members.includes(memberId)) return false;
|
|
503
|
+
if (!rule.pattern) return true;
|
|
504
|
+
const patterns = ctx.permissionPatterns ? ctx.permissionPatterns(args) : extractApprovalPatterns(toolName, args);
|
|
505
|
+
return patterns.some((pattern) => matchApprovalPattern(rule.pattern, pattern));
|
|
506
|
+
}
|
|
507
|
+
function teamPermissionPolicy(config) {
|
|
508
|
+
const rules = config.rules ?? [];
|
|
509
|
+
return {
|
|
510
|
+
name: `team-permission:${config.memberId}`,
|
|
511
|
+
async beforeToolCall(tool, args, ctx) {
|
|
512
|
+
if (resolveCapability(ctx.toolCapabilities?.readOnly, args, false)) {
|
|
513
|
+
return { action: "allow" };
|
|
514
|
+
}
|
|
515
|
+
for (const rule of rules) {
|
|
516
|
+
if (matchesRule(rule, config.memberId, tool, args, ctx)) {
|
|
517
|
+
if (rule.action === "deny") {
|
|
518
|
+
return {
|
|
519
|
+
action: "deny",
|
|
520
|
+
reason: rule.reason ?? `Denied by team policy for "${config.memberId}"`
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
return { action: "allow" };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (config.forwardApproval) {
|
|
527
|
+
return config.forwardApproval(config.memberId, tool, args, ctx);
|
|
528
|
+
}
|
|
529
|
+
return { action: "allow" };
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/team/execution.ts
|
|
535
|
+
function isTerminalStatus(status) {
|
|
536
|
+
return status === "completed" || status === "aborted" || status === "failed" || status === "cancelled";
|
|
537
|
+
}
|
|
538
|
+
function isQueuedMessageForMember(message, memberId) {
|
|
539
|
+
if (message.kind === "system") {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
if (message.kind === "broadcast") {
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
return message.to === memberId;
|
|
546
|
+
}
|
|
547
|
+
async function collectPendingMessages(ctx, runtime) {
|
|
548
|
+
const messages = await ctx.mailbox.list({
|
|
549
|
+
teamId: ctx.teamId,
|
|
550
|
+
kind: ["direct", "broadcast"]
|
|
551
|
+
});
|
|
552
|
+
const relevant = messages.filter((message) => isQueuedMessageForMember(message, runtime.member.id));
|
|
553
|
+
if (!runtime.lastDeliveredMessageAt || !runtime.lastDeliveredMessageId) {
|
|
554
|
+
return relevant;
|
|
555
|
+
}
|
|
556
|
+
const pending = [];
|
|
557
|
+
let cursorReached = false;
|
|
558
|
+
for (const message of relevant) {
|
|
559
|
+
if (cursorReached) {
|
|
560
|
+
pending.push(message);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (message.createdAt < runtime.lastDeliveredMessageAt) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (message.createdAt > runtime.lastDeliveredMessageAt) {
|
|
567
|
+
cursorReached = true;
|
|
568
|
+
pending.push(message);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (message.id === runtime.lastDeliveredMessageId) {
|
|
572
|
+
cursorReached = true;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return pending;
|
|
576
|
+
}
|
|
577
|
+
async function claimAndExecute(ctx, runtime) {
|
|
578
|
+
if (runtime.member.status !== "idle") return void 0;
|
|
579
|
+
const claimed = await ctx.taskBoard.claimNextTask(runtime.member.id);
|
|
580
|
+
if (!claimed) return void 0;
|
|
581
|
+
await ctx.updateMemberStatus(runtime.member.id, "busy", claimed.id);
|
|
582
|
+
await ctx.emitTransitions([{ task: claimed, reason: "claimed", previous: "pending" }]);
|
|
583
|
+
const started = await ctx.taskBoard.startTask(claimed.id, ctx.createId());
|
|
584
|
+
await ctx.emit({
|
|
585
|
+
type: "team-task-transition",
|
|
586
|
+
teamId: ctx.teamId,
|
|
587
|
+
taskId: started.id,
|
|
588
|
+
previous: claimed.status,
|
|
589
|
+
current: "running",
|
|
590
|
+
reason: "started"
|
|
591
|
+
});
|
|
592
|
+
const pendingMessages = await collectPendingMessages(ctx, runtime);
|
|
593
|
+
if (pendingMessages.length > 0) {
|
|
594
|
+
const latest = pendingMessages[pendingMessages.length - 1];
|
|
595
|
+
runtime.lastDeliveredMessageAt = latest.createdAt;
|
|
596
|
+
runtime.lastDeliveredMessageId = latest.id;
|
|
597
|
+
}
|
|
598
|
+
const prompt = buildTaskPrompt(runtime.role, started, pendingMessages);
|
|
599
|
+
runtime.taskAbort = new AbortController();
|
|
600
|
+
runtime.taskStartedAt = Date.now();
|
|
601
|
+
runtime.activeRun = executeTask(ctx, runtime, started, prompt).catch((err) => {
|
|
602
|
+
ctx.log?.(`[${runtime.member.id}] executeTask unhandled error: ${err instanceof Error ? err.message : String(err)}`);
|
|
603
|
+
});
|
|
604
|
+
return started;
|
|
605
|
+
}
|
|
606
|
+
async function prepareTaskForExternalExecution(ctx, runtime, taskId, runId) {
|
|
607
|
+
if (ctx.taskDispatchMode !== "external") {
|
|
608
|
+
throw new Error('External task preparation requires taskDispatchMode="external"');
|
|
609
|
+
}
|
|
610
|
+
if (runtime.member.status !== "idle") return void 0;
|
|
611
|
+
const task = await ctx.taskBoard.getTask(taskId);
|
|
612
|
+
if (!task || task.memberId !== runtime.member.id || task.status !== "pending") {
|
|
613
|
+
return void 0;
|
|
614
|
+
}
|
|
615
|
+
const claimed = await ctx.taskBoard.claimTask(taskId, runtime.member.id);
|
|
616
|
+
await ctx.updateMemberStatus(runtime.member.id, "busy", claimed.id);
|
|
617
|
+
await ctx.emitTransitions([{ task: claimed, reason: "claimed", previous: "pending" }]);
|
|
618
|
+
const started = await ctx.taskBoard.startTask(claimed.id, runId);
|
|
619
|
+
await ctx.emit({
|
|
620
|
+
type: "team-task-transition",
|
|
621
|
+
teamId: ctx.teamId,
|
|
622
|
+
taskId: started.id,
|
|
623
|
+
previous: claimed.status,
|
|
624
|
+
current: "running",
|
|
625
|
+
reason: "started"
|
|
626
|
+
});
|
|
627
|
+
const pendingMessages = await collectPendingMessages(ctx, runtime);
|
|
628
|
+
if (pendingMessages.length > 0) {
|
|
629
|
+
const latest = pendingMessages[pendingMessages.length - 1];
|
|
630
|
+
runtime.lastDeliveredMessageAt = latest.createdAt;
|
|
631
|
+
runtime.lastDeliveredMessageId = latest.id;
|
|
632
|
+
}
|
|
633
|
+
runtime.taskStartedAt = Date.now();
|
|
634
|
+
return {
|
|
635
|
+
task: started,
|
|
636
|
+
memberId: runtime.member.id,
|
|
637
|
+
memberRole: runtime.role.name,
|
|
638
|
+
sessionId: runtime.member.sessionId,
|
|
639
|
+
prompt: buildTaskPrompt(runtime.role, started, pendingMessages)
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
async function executeTask(ctx, runtime, task, prompt) {
|
|
643
|
+
try {
|
|
644
|
+
const result = ctx.taskExecutor ? await ctx.taskExecutor({
|
|
645
|
+
runtime,
|
|
646
|
+
task,
|
|
647
|
+
prompt,
|
|
648
|
+
parentSessionId: ctx.leadSessionId,
|
|
649
|
+
abort: runtime.taskAbort?.signal
|
|
650
|
+
}) : await runtime.agent.run({
|
|
651
|
+
parentSessionId: ctx.leadSessionId,
|
|
652
|
+
message: prompt,
|
|
653
|
+
title: task.title,
|
|
654
|
+
abort: runtime.taskAbort?.signal
|
|
655
|
+
});
|
|
656
|
+
const taskResult = {
|
|
657
|
+
response: result.response,
|
|
658
|
+
usage: result.usage,
|
|
659
|
+
toolCalls: result.toolCalls
|
|
660
|
+
};
|
|
661
|
+
await completePreparedTask(ctx, runtime, task.id, taskResult);
|
|
662
|
+
} catch (err) {
|
|
663
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
664
|
+
const wasAborted = runtime.taskAbort?.signal.aborted;
|
|
665
|
+
try {
|
|
666
|
+
if (wasAborted) {
|
|
667
|
+
await abortPreparedTask(
|
|
668
|
+
ctx,
|
|
669
|
+
runtime,
|
|
670
|
+
task.id,
|
|
671
|
+
String(runtime.taskAbort?.signal.reason ?? error)
|
|
672
|
+
);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
await failPreparedTask(ctx, runtime, task.id, error);
|
|
676
|
+
} catch (inner) {
|
|
677
|
+
ctx.log?.(`[${runtime.member.id}] task finalization error: ${inner instanceof Error ? inner.message : String(inner)}`);
|
|
678
|
+
}
|
|
679
|
+
} finally {
|
|
680
|
+
try {
|
|
681
|
+
await finalizePreparedTask(ctx, runtime, true);
|
|
682
|
+
} catch (cleanupErr) {
|
|
683
|
+
ctx.log?.(`[${runtime.member.id}] finalizePreparedTask error: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`);
|
|
684
|
+
runtime.activeRun = void 0;
|
|
685
|
+
runtime.taskAbort = void 0;
|
|
686
|
+
runtime.taskStartedAt = void 0;
|
|
687
|
+
try {
|
|
688
|
+
await ctx.updateMemberStatus(runtime.member.id, "idle", void 0);
|
|
689
|
+
} catch {
|
|
690
|
+
runtime.member.status = "idle";
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async function completePreparedTask(ctx, runtime, taskId, taskResult) {
|
|
696
|
+
const latest = await beginTaskFinalization(ctx, runtime, taskId);
|
|
697
|
+
if (!latest) return void 0;
|
|
698
|
+
runtime.stats.tasksCompleted += 1;
|
|
699
|
+
runtime.stats.totalToolCalls += taskResult.toolCalls?.length ?? 0;
|
|
700
|
+
if (taskResult.usage) {
|
|
701
|
+
runtime.stats.totalTokens = {
|
|
702
|
+
inputTokens: (runtime.stats.totalTokens.inputTokens ?? 0) + (taskResult.usage.inputTokens ?? 0),
|
|
703
|
+
outputTokens: (runtime.stats.totalTokens.outputTokens ?? 0) + (taskResult.usage.outputTokens ?? 0),
|
|
704
|
+
totalTokens: (runtime.stats.totalTokens.totalTokens ?? 0) + (taskResult.usage.totalTokens ?? 0)
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
const { task, transitions } = await ctx.taskBoard.completeTask(taskId, taskResult);
|
|
708
|
+
await ctx.emit({
|
|
709
|
+
type: "team-task-completed",
|
|
710
|
+
teamId: ctx.teamId,
|
|
711
|
+
taskId,
|
|
712
|
+
memberId: runtime.member.id,
|
|
713
|
+
result: taskResult
|
|
714
|
+
});
|
|
715
|
+
await ctx.emitTransitions(transitions);
|
|
716
|
+
return { task, transitions };
|
|
717
|
+
}
|
|
718
|
+
async function failPreparedTask(ctx, runtime, taskId, error) {
|
|
719
|
+
const latest = await beginTaskFinalization(ctx, runtime, taskId);
|
|
720
|
+
if (!latest) return void 0;
|
|
721
|
+
runtime.stats.tasksFailed += 1;
|
|
722
|
+
const { task, transitions } = await ctx.taskBoard.failTask(taskId, error);
|
|
723
|
+
await ctx.emit({
|
|
724
|
+
type: "team-task-failed",
|
|
725
|
+
teamId: ctx.teamId,
|
|
726
|
+
taskId,
|
|
727
|
+
memberId: runtime.member.id,
|
|
728
|
+
error
|
|
729
|
+
});
|
|
730
|
+
await ctx.emitTransitions(transitions);
|
|
731
|
+
return { task, transitions };
|
|
732
|
+
}
|
|
733
|
+
async function abortPreparedTask(ctx, runtime, taskId, reason) {
|
|
734
|
+
const latest = await beginTaskFinalization(ctx, runtime, taskId);
|
|
735
|
+
if (!latest) return void 0;
|
|
736
|
+
const { task, transitions } = await ctx.taskBoard.abortTask(taskId, reason);
|
|
737
|
+
await ctx.emit({
|
|
738
|
+
type: "team-task-aborted",
|
|
739
|
+
teamId: ctx.teamId,
|
|
740
|
+
taskId,
|
|
741
|
+
memberId: runtime.member.id,
|
|
742
|
+
reason
|
|
743
|
+
});
|
|
744
|
+
await ctx.emitTransitions(transitions);
|
|
745
|
+
return { task, transitions };
|
|
746
|
+
}
|
|
747
|
+
function buildTaskPrompt(role, task, pendingMessages = []) {
|
|
748
|
+
const queuedMessages = pendingMessages.length > 0 ? "\n\nTeam messages to consider before you start:\n" + pendingMessages.map((message) => `- [${message.kind}] ${message.from}: ${message.body}`).join("\n") : "";
|
|
749
|
+
return `You are the "${role.name}" specialist.
|
|
750
|
+
|
|
751
|
+
Role: ${role.description}
|
|
752
|
+
|
|
753
|
+
Task: ${task.title}
|
|
754
|
+
|
|
755
|
+
` + task.prompt + queuedMessages;
|
|
756
|
+
}
|
|
757
|
+
async function beginTaskFinalization(ctx, runtime, taskId) {
|
|
758
|
+
const duration = runtime.taskStartedAt ? Date.now() - runtime.taskStartedAt : 0;
|
|
759
|
+
runtime.stats.totalDurationMs += duration;
|
|
760
|
+
runtime.stats.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
761
|
+
const latest = await ctx.taskBoard.getTask(taskId);
|
|
762
|
+
if (!latest || isTerminalStatus(latest.status)) {
|
|
763
|
+
return void 0;
|
|
764
|
+
}
|
|
765
|
+
return latest;
|
|
766
|
+
}
|
|
767
|
+
async function finalizePreparedTask(ctx, runtime, clearAbortController) {
|
|
768
|
+
runtime.activeRun = void 0;
|
|
769
|
+
if (clearAbortController) {
|
|
770
|
+
runtime.taskAbort = void 0;
|
|
771
|
+
}
|
|
772
|
+
runtime.taskStartedAt = void 0;
|
|
773
|
+
if (runtime.stopRequested || runtime.member.status === "shutting-down") {
|
|
774
|
+
await ctx.updateMemberStatus(runtime.member.id, "offline", void 0);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
await ctx.updateMemberStatus(runtime.member.id, "idle", void 0);
|
|
778
|
+
if (ctx.taskDispatchMode === "manual") {
|
|
779
|
+
await ctx.dispatchReady();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/team/work-loop.ts
|
|
784
|
+
async function runWorkLoop(ctx, runtime) {
|
|
785
|
+
while (!runtime.stopRequested && ctx.started) {
|
|
786
|
+
if (runtime.member.status === "idle") {
|
|
787
|
+
if (ctx.beforeIteration) {
|
|
788
|
+
try {
|
|
789
|
+
const skip = await ctx.beforeIteration(
|
|
790
|
+
runtime.member.id,
|
|
791
|
+
runtime.agent,
|
|
792
|
+
runtime.stats
|
|
793
|
+
);
|
|
794
|
+
if (skip === false) {
|
|
795
|
+
await sleep(ctx.pollIntervalMs);
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
} catch (err) {
|
|
799
|
+
ctx.log?.(`[${runtime.member.id}] beforeIteration error: ${err instanceof Error ? err.message : String(err)}`);
|
|
800
|
+
await sleep(ctx.pollIntervalMs);
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const task = await tryClaimAndExecute(ctx, runtime);
|
|
805
|
+
if (task) {
|
|
806
|
+
if (runtime.activeRun) {
|
|
807
|
+
try {
|
|
808
|
+
await runtime.activeRun;
|
|
809
|
+
} catch (err) {
|
|
810
|
+
ctx.log?.(`[${runtime.member.id}] work-loop: task error \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (runtime.member.status === "busy" && runtime.activeRun) {
|
|
817
|
+
try {
|
|
818
|
+
await runtime.activeRun;
|
|
819
|
+
} catch (err) {
|
|
820
|
+
ctx.log?.(`[${runtime.member.id}] work-loop: active run error \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
821
|
+
}
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
await sleep(ctx.pollIntervalMs);
|
|
825
|
+
}
|
|
826
|
+
if (runtime.member.status !== "offline") {
|
|
827
|
+
await ctx.updateMemberStatus(runtime.member.id, "offline", void 0);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
async function tryClaimAndExecute(ctx, runtime) {
|
|
831
|
+
return claimAndExecute(ctx, runtime);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/team/shutdown.ts
|
|
835
|
+
async function requestShutdown(ctx, memberId, reason, timeoutMs) {
|
|
836
|
+
const runtime = ctx.members.get(memberId);
|
|
837
|
+
if (!runtime) {
|
|
838
|
+
throw new Error(
|
|
839
|
+
`Unknown member: "${memberId}". Registered: ${[...ctx.members.keys()].join(", ")}`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
if (runtime.member.status === "offline") return true;
|
|
843
|
+
if (runtime.member.status === "shutting-down") {
|
|
844
|
+
return waitForOffline(runtime, timeoutMs);
|
|
845
|
+
}
|
|
846
|
+
const requestId = ctx.createId();
|
|
847
|
+
runtime.pendingShutdownId = requestId;
|
|
848
|
+
runtime.stopRequested = true;
|
|
849
|
+
await ctx.emit({
|
|
850
|
+
type: "team-shutdown-requested",
|
|
851
|
+
teamId: ctx.teamId,
|
|
852
|
+
memberId,
|
|
853
|
+
requestId,
|
|
854
|
+
reason
|
|
855
|
+
});
|
|
856
|
+
await ctx.mailbox.send({
|
|
857
|
+
teamId: ctx.teamId,
|
|
858
|
+
from: "coordinator",
|
|
859
|
+
to: memberId,
|
|
860
|
+
kind: "system",
|
|
861
|
+
body: reason ?? "Shutdown requested by coordinator.",
|
|
862
|
+
payload: {
|
|
863
|
+
kind: "shutdown-request",
|
|
864
|
+
requestId,
|
|
865
|
+
reason
|
|
866
|
+
},
|
|
867
|
+
metadata: { payloadKind: "shutdown-request", requestId }
|
|
868
|
+
});
|
|
869
|
+
if (runtime.member.status === "idle") {
|
|
870
|
+
await ctx.updateMemberStatus(memberId, "offline", void 0);
|
|
871
|
+
await emitShutdownResolved(ctx, memberId, requestId, true);
|
|
872
|
+
runtime.pendingShutdownId = void 0;
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
await ctx.updateMemberStatus(memberId, "shutting-down", runtime.member.activeTaskId);
|
|
876
|
+
const accepted = await waitForOffline(runtime, timeoutMs);
|
|
877
|
+
await emitShutdownResolved(ctx, memberId, requestId, accepted);
|
|
878
|
+
runtime.pendingShutdownId = void 0;
|
|
879
|
+
return accepted;
|
|
880
|
+
}
|
|
881
|
+
async function shutdownAll(ctx, reason, timeoutMs) {
|
|
882
|
+
const ids = [...ctx.members.keys()].filter((id) => {
|
|
883
|
+
const rt = ctx.members.get(id);
|
|
884
|
+
return rt && rt.member.status !== "offline";
|
|
885
|
+
});
|
|
886
|
+
await Promise.all(
|
|
887
|
+
ids.map((id) => requestShutdown(ctx, id, reason, timeoutMs))
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
async function waitForOffline(runtime, timeoutMs) {
|
|
891
|
+
const deadline = timeoutMs !== void 0 ? Date.now() + timeoutMs : void 0;
|
|
892
|
+
while (runtime.member.status !== "offline") {
|
|
893
|
+
if (deadline && Date.now() >= deadline) return false;
|
|
894
|
+
await sleep(100);
|
|
895
|
+
}
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
async function emitShutdownResolved(ctx, memberId, requestId, accepted) {
|
|
899
|
+
await ctx.emit({
|
|
900
|
+
type: "team-shutdown-resolved",
|
|
901
|
+
teamId: ctx.teamId,
|
|
902
|
+
memberId,
|
|
903
|
+
requestId,
|
|
904
|
+
accepted
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/team/coordinator/turn.ts
|
|
909
|
+
import { z } from "zod";
|
|
910
|
+
|
|
911
|
+
// src/team/coordinator/policy.ts
|
|
912
|
+
var coordinatorToolDescriptions = {
|
|
913
|
+
assignTask(memberList) {
|
|
914
|
+
return `Assign a task to a teammate. The teammate receives ONLY the prompt you provide \u2014 include all necessary context, file paths, and instructions. Available teammates: ${memberList}`;
|
|
915
|
+
},
|
|
916
|
+
sendMessage: "Send guidance to a teammate. If they're working, the message is injected immediately. If they're idle, it's queued for their next task.",
|
|
917
|
+
abortTask: "Abort a running task without cascading to blocked dependents. The teammate returns to idle and can be assigned new work. Use this when a task is taking the wrong approach or should be restarted."
|
|
918
|
+
};
|
|
919
|
+
function buildCoordinatorSystemPrompt(members) {
|
|
920
|
+
const roster = members.map(
|
|
921
|
+
(member) => `- **${member.id}** (${member.role}): ${member.description ?? "General purpose agent"}`
|
|
922
|
+
).join("\n");
|
|
923
|
+
return "You are the team coordinator. Your job is to break down tasks, delegate work to your teammates, synthesize their results, and communicate the final answer to the user.\n\n## Your Teammates\n\n" + roster + "\n\n## How It Works\n\n1. You receive the user's request.\n2. Decide what work to delegate. Use `assignTask` to give each teammate a focused, self-contained task.\n3. After teammates finish, you'll receive their results as [Task Notification] messages.\n4. Teammates may also send you [Worker Report] messages with mid-task findings or blockers.\n5. Based on results and reports, you may assign follow-up tasks, send guidance via `sendMessage`, or produce your final answer.\n\n## Guidelines\n\n- **Answer directly** when the question is simple and doesn't need tool use.\n- **Delegate** when work requires reading files, writing code, running tests, or exploration.\n- **After assigning tasks, briefly state what you assigned and end your response.** Do not wait for, predict, or fabricate task results \u2014 they will arrive as [Task Notification] messages.\n- **Write self-contained prompts.** Each teammate sees ONLY their task prompt. Include specific file paths, line numbers, and all context they need. Never say 'based on the research' \u2014 include the actual findings.\n- **Parallelize** independent tasks. Assign multiple teammates in one turn when their work doesn't depend on each other.\n- **Use dependencies** when tasks must run in order. Pass `dependsOn: [taskId]` to block a task until its prerequisites complete. Use the task IDs returned by previous assignTask calls.\n- **Serialize** dependent tasks. Wait for research results before assigning implementation work, or use `dependsOn` to chain them.\n- **Synthesize before acting.** When research results come back, read and understand them before directing follow-up work.\n- **Do not repeat completed work.** If a teammate has already completed a task and their result answers the need, synthesize it into your response instead of assigning the same task again.\n- **Only reassign after completion when there is a concrete gap.** If the result is missing, failed, clearly incomplete, or you need a different follow-up phase, explain that gap and assign a narrower next task.\n- **Teammates retain context.** If you assign the same teammate a second task, they remember their prior work. Use this for research-then-implement flows on the same teammate.\n- **On failure:** Review the error. If context is useful, assign the same teammate again. If the approach was wrong, assign a different teammate or try a new strategy.\n\n## Workflow Phases\n\nFor complex tasks, follow this pattern:\n1. **Research** \u2014 Explore the codebase, read files, understand the problem\n2. **Synthesize** \u2014 Read research results and form a concrete plan with specific files and changes\n3. **Implement** \u2014 Direct implementation with exact instructions including file paths and code\n4. **Verify** \u2014 Run tests, check the output, confirm the changes work\n\nWhen you have received all results and have nothing more to delegate, respond with your final comprehensive answer to the user.";
|
|
924
|
+
}
|
|
925
|
+
function formatCoordinatorTaskNotifications(notifications) {
|
|
926
|
+
if (notifications.length === 0) return "";
|
|
927
|
+
return notifications.map((notification) => {
|
|
928
|
+
let resultText = `Status: ${notification.status}`;
|
|
929
|
+
if (notification.status === "completed" && notification.result?.response) {
|
|
930
|
+
resultText = notification.result.response;
|
|
931
|
+
} else if (notification.status === "failed") {
|
|
932
|
+
resultText = `Error: ${notification.error ?? "Unknown error"}`;
|
|
933
|
+
} else if (notification.status === "aborted") {
|
|
934
|
+
resultText = `Aborted${notification.error ? `: ${notification.error}` : ""}`;
|
|
935
|
+
} else if (notification.error) {
|
|
936
|
+
resultText = notification.error;
|
|
937
|
+
}
|
|
938
|
+
return `[Task Notification]
|
|
939
|
+
Task: ${notification.taskId} (${notification.memberId})
|
|
940
|
+
Title: ${notification.title}
|
|
941
|
+
Status: ${notification.status}
|
|
942
|
+
Result:
|
|
943
|
+
${resultText}`;
|
|
944
|
+
}).join("\n\n---\n\n");
|
|
945
|
+
}
|
|
946
|
+
function formatCoordinatorWorkerReports(reports) {
|
|
947
|
+
if (reports.length === 0) return "";
|
|
948
|
+
return reports.map(
|
|
949
|
+
(report) => `[Worker Report]
|
|
950
|
+
From: ${report.memberId}
|
|
951
|
+
${report.taskId ? `Task: ${report.taskId}
|
|
952
|
+
` : ""}Message:
|
|
953
|
+
${report.message}`
|
|
954
|
+
).join("\n\n---\n\n");
|
|
955
|
+
}
|
|
956
|
+
function formatCoordinatorRoundMessage(notifications, reports) {
|
|
957
|
+
const parts = [];
|
|
958
|
+
const taskText = formatCoordinatorTaskNotifications(notifications);
|
|
959
|
+
const reportText = formatCoordinatorWorkerReports(reports);
|
|
960
|
+
if (taskText) parts.push(taskText);
|
|
961
|
+
if (reportText) parts.push(reportText);
|
|
962
|
+
return parts.join("\n\n---\n\n");
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/team/coordinator/turn.ts
|
|
966
|
+
var ASK_COORDINATOR_TIMEOUT_MS = 9e4;
|
|
967
|
+
function createAskCoordinatorTool(deps) {
|
|
968
|
+
const { memberId, teamId, runtime, mailbox } = deps;
|
|
969
|
+
return Tool.define("askCoordinator", {
|
|
970
|
+
description: "Ask the coordinator a question and wait for a response before continuing. Use this ONLY when you genuinely need guidance to proceed \u2014 for example, you've hit an ambiguity, discovered something that changes the task scope, or need clarification on requirements. Do NOT use this for progress updates; just continue working and report results when you finish.",
|
|
971
|
+
capabilities: { readOnly: true },
|
|
972
|
+
parameters: z.object({
|
|
973
|
+
question: z.string().describe(
|
|
974
|
+
"Your question for the coordinator \u2014 be specific about what you need to know"
|
|
975
|
+
)
|
|
976
|
+
}),
|
|
977
|
+
execute: async (params) => {
|
|
978
|
+
await mailbox.send({
|
|
979
|
+
teamId,
|
|
980
|
+
from: memberId,
|
|
981
|
+
to: "coordinator",
|
|
982
|
+
kind: "direct",
|
|
983
|
+
body: params.question,
|
|
984
|
+
payload: { kind: "worker-report" },
|
|
985
|
+
metadata: { payloadKind: "worker-report" }
|
|
986
|
+
});
|
|
987
|
+
const response = await new Promise((resolve) => {
|
|
988
|
+
const timeout = setTimeout(() => {
|
|
989
|
+
runtime.pendingAskResolve = void 0;
|
|
990
|
+
resolve("");
|
|
991
|
+
}, ASK_COORDINATOR_TIMEOUT_MS);
|
|
992
|
+
runtime.pendingAskResolve = (msg) => {
|
|
993
|
+
clearTimeout(timeout);
|
|
994
|
+
runtime.pendingAskResolve = void 0;
|
|
995
|
+
resolve(msg);
|
|
996
|
+
};
|
|
997
|
+
});
|
|
998
|
+
if (!response) {
|
|
999
|
+
return {
|
|
1000
|
+
title: "No response from coordinator",
|
|
1001
|
+
output: "The coordinator hasn't responded yet. Use your best judgment and continue working on the task.",
|
|
1002
|
+
metadata: { truncated: false }
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
return {
|
|
1006
|
+
title: "Coordinator response",
|
|
1007
|
+
output: response,
|
|
1008
|
+
metadata: { truncated: false }
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
function isAbortError(err) {
|
|
1014
|
+
if (err instanceof DOMException && err.name === "AbortError") return true;
|
|
1015
|
+
if (err instanceof Error && err.name === "AbortError") return true;
|
|
1016
|
+
if (err instanceof Error && err.message?.includes("aborted")) return true;
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
async function* raceAbort(source, signal) {
|
|
1020
|
+
if (signal.aborted) return;
|
|
1021
|
+
const aborted = new Promise((resolve) => {
|
|
1022
|
+
signal.addEventListener(
|
|
1023
|
+
"abort",
|
|
1024
|
+
() => resolve({ done: true, value: void 0 }),
|
|
1025
|
+
{ once: true }
|
|
1026
|
+
);
|
|
1027
|
+
});
|
|
1028
|
+
try {
|
|
1029
|
+
while (true) {
|
|
1030
|
+
const result = await Promise.race([source.next(), aborted]);
|
|
1031
|
+
if (result.done) return;
|
|
1032
|
+
yield result.value;
|
|
1033
|
+
}
|
|
1034
|
+
} finally {
|
|
1035
|
+
const cleanup = source.return(void 0);
|
|
1036
|
+
await Promise.race([
|
|
1037
|
+
cleanup,
|
|
1038
|
+
new Promise((resolve) => setTimeout(resolve, 5e3))
|
|
1039
|
+
]).catch(() => {
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function formatNotification(notification) {
|
|
1044
|
+
switch (notification.kind) {
|
|
1045
|
+
case "worker-report":
|
|
1046
|
+
return `[Worker Report]
|
|
1047
|
+
From: ${notification.memberId}
|
|
1048
|
+
${notification.taskId ? `Task: ${notification.taskId}
|
|
1049
|
+
` : ""}Message:
|
|
1050
|
+
${notification.body}`;
|
|
1051
|
+
case "task-result":
|
|
1052
|
+
return `[Task Notification]
|
|
1053
|
+
Task: ${notification.taskId ?? "unknown"} (${notification.memberId})
|
|
1054
|
+
Title: ${notification.title}
|
|
1055
|
+
Status: ${notification.status ?? "unknown"}
|
|
1056
|
+
${notification.body}`;
|
|
1057
|
+
default:
|
|
1058
|
+
return `[Coordinator Notification]
|
|
1059
|
+
From: ${notification.memberId}
|
|
1060
|
+
Kind: ${notification.kind}
|
|
1061
|
+
${notification.body}`;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function formatCoordinatorInboxBatch(items) {
|
|
1065
|
+
return items.map(
|
|
1066
|
+
(item) => item.kind === "user-message" ? item.body : formatNotification(item.notification ?? {
|
|
1067
|
+
id: item.id,
|
|
1068
|
+
teamId: "",
|
|
1069
|
+
memberId: "unknown",
|
|
1070
|
+
kind: "worker-report",
|
|
1071
|
+
priority: "immediate",
|
|
1072
|
+
title: "Notification",
|
|
1073
|
+
body: item.body,
|
|
1074
|
+
createdAt: item.createdAt
|
|
1075
|
+
})
|
|
1076
|
+
).join("\n\n---\n\n");
|
|
1077
|
+
}
|
|
1078
|
+
function createCoordinatorTools(coordinator) {
|
|
1079
|
+
const memberIds = [...coordinator.members.keys()];
|
|
1080
|
+
const memberList = memberIds.join(", ");
|
|
1081
|
+
const assignTask = Tool.define("assignTask", {
|
|
1082
|
+
description: coordinatorToolDescriptions.assignTask(memberList),
|
|
1083
|
+
parameters: z.object({
|
|
1084
|
+
memberId: z.string().describe("Teammate ID to assign the task to"),
|
|
1085
|
+
title: z.string().describe("Short task title (2-6 words)"),
|
|
1086
|
+
prompt: z.string().describe(
|
|
1087
|
+
"Self-contained task prompt with all context the teammate needs"
|
|
1088
|
+
),
|
|
1089
|
+
dependsOn: z.array(z.string()).optional().describe(
|
|
1090
|
+
"Task IDs this task must wait for before starting."
|
|
1091
|
+
)
|
|
1092
|
+
}),
|
|
1093
|
+
capabilities: { readOnly: true },
|
|
1094
|
+
execute: async (params) => {
|
|
1095
|
+
const state = coordinator.getActiveCoordinatorTurnState();
|
|
1096
|
+
const task = await coordinator.queue({
|
|
1097
|
+
memberId: params.memberId,
|
|
1098
|
+
title: params.title,
|
|
1099
|
+
prompt: params.prompt,
|
|
1100
|
+
dependsOn: params.dependsOn
|
|
1101
|
+
});
|
|
1102
|
+
state.actions.assigned.push({
|
|
1103
|
+
taskId: task.id,
|
|
1104
|
+
memberId: params.memberId,
|
|
1105
|
+
title: params.title
|
|
1106
|
+
});
|
|
1107
|
+
return {
|
|
1108
|
+
title: `Assigned to ${params.memberId}`,
|
|
1109
|
+
output: `Task ${task.id} assigned to ${params.memberId}: "${params.title}"`,
|
|
1110
|
+
metadata: {}
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
const sendMessage = Tool.define("sendMessage", {
|
|
1115
|
+
description: coordinatorToolDescriptions.sendMessage,
|
|
1116
|
+
parameters: z.object({
|
|
1117
|
+
memberId: z.string().describe("Teammate ID to message"),
|
|
1118
|
+
message: z.string().describe("Guidance or instructions to send")
|
|
1119
|
+
}),
|
|
1120
|
+
capabilities: { readOnly: true },
|
|
1121
|
+
execute: async (params) => {
|
|
1122
|
+
const state = coordinator.getActiveCoordinatorTurnState();
|
|
1123
|
+
await coordinator.guide(params.memberId, params.message);
|
|
1124
|
+
state.actions.sentMessages.push({ memberId: params.memberId });
|
|
1125
|
+
return {
|
|
1126
|
+
title: `Messaged ${params.memberId}`,
|
|
1127
|
+
output: `Message sent to ${params.memberId}`,
|
|
1128
|
+
metadata: {}
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
const abortTask = Tool.define("abortTask", {
|
|
1133
|
+
description: coordinatorToolDescriptions.abortTask,
|
|
1134
|
+
parameters: z.object({
|
|
1135
|
+
taskId: z.string().describe("ID of the task to abort"),
|
|
1136
|
+
reason: z.string().optional().describe("Why the task is being aborted")
|
|
1137
|
+
}),
|
|
1138
|
+
capabilities: { readOnly: true },
|
|
1139
|
+
execute: async (params) => {
|
|
1140
|
+
const state = coordinator.getActiveCoordinatorTurnState();
|
|
1141
|
+
await coordinator.abort(params.taskId, params.reason);
|
|
1142
|
+
state.actions.abortedTaskIds.push(params.taskId);
|
|
1143
|
+
return {
|
|
1144
|
+
title: "Task aborted",
|
|
1145
|
+
output: `Task ${params.taskId} aborted${params.reason ? `: ${params.reason}` : ""}`,
|
|
1146
|
+
metadata: {}
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
return [assignTask, sendMessage, abortTask];
|
|
1151
|
+
}
|
|
1152
|
+
function createCoordinatorAgent(coordinator) {
|
|
1153
|
+
return coordinator.lead.fork({
|
|
1154
|
+
name: "coordinator",
|
|
1155
|
+
systemPrompt: buildCoordinatorSystemPrompt(
|
|
1156
|
+
[...coordinator.members.values()].map((runtime) => ({
|
|
1157
|
+
id: runtime.member.id,
|
|
1158
|
+
role: runtime.role.name,
|
|
1159
|
+
description: runtime.role.description
|
|
1160
|
+
}))
|
|
1161
|
+
),
|
|
1162
|
+
tools: createCoordinatorTools(coordinator),
|
|
1163
|
+
reasoningLevel: "off",
|
|
1164
|
+
maxSteps: 1
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
async function runCoordinatorTurn(coordinator, agent, sessionId, message, options) {
|
|
1168
|
+
const log = options?.onLog ?? (() => {
|
|
1169
|
+
});
|
|
1170
|
+
const onEvent = options?.onEvent;
|
|
1171
|
+
const onStatus = options?.onStatus;
|
|
1172
|
+
const roundAbort = new AbortController();
|
|
1173
|
+
const state = coordinator.beginCoordinatorTurnState();
|
|
1174
|
+
let response = "";
|
|
1175
|
+
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
1176
|
+
const toolCalls = [];
|
|
1177
|
+
let hasDelegated = false;
|
|
1178
|
+
let inactivityTimer = null;
|
|
1179
|
+
const INACTIVITY_MS_NORMAL = 9e4;
|
|
1180
|
+
const INACTIVITY_MS_DELEGATED = 15e3;
|
|
1181
|
+
const clearTimers = () => {
|
|
1182
|
+
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
1183
|
+
inactivityTimer = null;
|
|
1184
|
+
};
|
|
1185
|
+
const resetInactivityTimer = () => {
|
|
1186
|
+
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
1187
|
+
const ms = hasDelegated ? INACTIVITY_MS_DELEGATED : INACTIVITY_MS_NORMAL;
|
|
1188
|
+
inactivityTimer = setTimeout(() => {
|
|
1189
|
+
log(`Aborting coordinator stream after ${ms / 1e3}s inactivity (delegated=${hasDelegated})`);
|
|
1190
|
+
roundAbort.abort();
|
|
1191
|
+
}, ms);
|
|
1192
|
+
};
|
|
1193
|
+
coordinator.attachCoordinatorTurn(agent, sessionId);
|
|
1194
|
+
coordinator.setCoordinatorTurnOpen(true);
|
|
1195
|
+
resetInactivityTimer();
|
|
1196
|
+
log(`runCoordinatorTurn: start (msg=${message.length}ch)`);
|
|
1197
|
+
try {
|
|
1198
|
+
onStatus?.({
|
|
1199
|
+
phase: "running",
|
|
1200
|
+
message: "Coordinator processing inbox...",
|
|
1201
|
+
pendingInboxCount: coordinator.getCoordinatorInboxSize(),
|
|
1202
|
+
activeTaskCount: await coordinator.getActiveTaskCount()
|
|
1203
|
+
});
|
|
1204
|
+
for await (const event of raceAbort(agent.chat(sessionId, message, { abort: roundAbort.signal }), roundAbort.signal)) {
|
|
1205
|
+
resetInactivityTimer();
|
|
1206
|
+
if (event.type !== "text-delta" && event.type !== "reasoning-delta") {
|
|
1207
|
+
const detail = event.type === "tool-start" ? ` ${event.toolName}` : event.type === "tool-result" ? ` ${event.toolName}` : event.type === "error" ? ` ${event.error?.message?.slice(0, 80)}` : event.type === "status" ? ` ${event.status}` : "";
|
|
1208
|
+
log(`stream: ${event.type}${detail}`);
|
|
1209
|
+
}
|
|
1210
|
+
const isMaxStepsNoise = event.type === "status" && event.status === "error" && hasDelegated || event.type === "error" && event.error?.message?.includes("Maximum steps");
|
|
1211
|
+
const isAbortNoise = roundAbort.signal.aborted && (event.type === "status" && event.status === "error" || event.type === "error" && isAbortError(event.error));
|
|
1212
|
+
if (onEvent && !isMaxStepsNoise && !isAbortNoise) {
|
|
1213
|
+
onEvent(event);
|
|
1214
|
+
}
|
|
1215
|
+
if (event.type === "tool-start") {
|
|
1216
|
+
const isCoordinatorTool = ["assignTask", "sendMessage", "abortTask"].includes(event.toolName);
|
|
1217
|
+
if (isCoordinatorTool) {
|
|
1218
|
+
hasDelegated = true;
|
|
1219
|
+
resetInactivityTimer();
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
switch (event.type) {
|
|
1223
|
+
case "text-delta":
|
|
1224
|
+
response += event.text;
|
|
1225
|
+
break;
|
|
1226
|
+
case "tool-result":
|
|
1227
|
+
toolCalls.push({ name: event.toolName, result: event.result });
|
|
1228
|
+
break;
|
|
1229
|
+
case "step-finish":
|
|
1230
|
+
if (event.usage) usage = event.usage;
|
|
1231
|
+
break;
|
|
1232
|
+
case "complete":
|
|
1233
|
+
if (event.usage) usage = event.usage;
|
|
1234
|
+
if (event.output !== void 0) response = event.output;
|
|
1235
|
+
break;
|
|
1236
|
+
case "error":
|
|
1237
|
+
if (event.error?.message?.includes("Maximum steps")) {
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
if (isAbortError(event.error) && roundAbort.signal.aborted) {
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
throw event.error;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
if (!(isAbortError(error) && roundAbort.signal.aborted)) {
|
|
1248
|
+
log(`runCoordinatorTurn: error \u2014 ${error instanceof Error ? error.message : String(error)}`);
|
|
1249
|
+
throw error;
|
|
1250
|
+
}
|
|
1251
|
+
log(`runCoordinatorTurn: aborted (expected)`);
|
|
1252
|
+
} finally {
|
|
1253
|
+
clearTimers();
|
|
1254
|
+
coordinator.setCoordinatorTurnOpen(false);
|
|
1255
|
+
if (roundAbort.signal.aborted) {
|
|
1256
|
+
agent.clearSessionLock(sessionId);
|
|
1257
|
+
}
|
|
1258
|
+
coordinator.endCoordinatorTurnState();
|
|
1259
|
+
}
|
|
1260
|
+
log(`runCoordinatorTurn: done \u2014 delegated=${hasDelegated} response=${response.length}ch assigned=${state.actions.assigned.length} msgs=${state.actions.sentMessages.length}`);
|
|
1261
|
+
return {
|
|
1262
|
+
response,
|
|
1263
|
+
usage,
|
|
1264
|
+
toolCalls,
|
|
1265
|
+
delegated: hasDelegated,
|
|
1266
|
+
actions: {
|
|
1267
|
+
assigned: [...state.actions.assigned],
|
|
1268
|
+
sentMessages: [...state.actions.sentMessages],
|
|
1269
|
+
abortedTaskIds: [...state.actions.abortedTaskIds]
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// src/team/notifications.ts
|
|
1275
|
+
function formatTaskBody(task, fallbackStatus, fallbackDetail) {
|
|
1276
|
+
const title = task?.title || "Untitled task";
|
|
1277
|
+
const detail = task?.status === "completed" && task.result?.response ? task.result.response : task?.status === "failed" ? task.error ?? fallbackDetail : task?.status === "aborted" ? task.error ?? fallbackDetail : task?.status === "cancelled" ? task.error ?? fallbackDetail : fallbackDetail;
|
|
1278
|
+
return `Title: ${title}
|
|
1279
|
+
Status: ${task?.status ?? fallbackStatus}
|
|
1280
|
+
Result:
|
|
1281
|
+
${detail}`;
|
|
1282
|
+
}
|
|
1283
|
+
function buildWorkerReportNotification(event) {
|
|
1284
|
+
if (event.message.payload?.kind !== "worker-report") {
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
return {
|
|
1288
|
+
type: "team-notification",
|
|
1289
|
+
teamId: event.teamId,
|
|
1290
|
+
notification: {
|
|
1291
|
+
id: `worker-report:${event.message.id}`,
|
|
1292
|
+
teamId: event.teamId,
|
|
1293
|
+
memberId: event.message.from,
|
|
1294
|
+
kind: "worker-report",
|
|
1295
|
+
priority: "immediate",
|
|
1296
|
+
title: "Worker report",
|
|
1297
|
+
body: event.message.body,
|
|
1298
|
+
createdAt: event.message.createdAt,
|
|
1299
|
+
taskId: event.message.taskId
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function buildPermissionForwardedNotification(event) {
|
|
1304
|
+
return {
|
|
1305
|
+
type: "team-notification",
|
|
1306
|
+
teamId: event.teamId,
|
|
1307
|
+
notification: {
|
|
1308
|
+
id: `permission-forwarded:${event.requestId}`,
|
|
1309
|
+
teamId: event.teamId,
|
|
1310
|
+
memberId: event.memberId,
|
|
1311
|
+
kind: "permission-forwarded",
|
|
1312
|
+
priority: "immediate",
|
|
1313
|
+
title: `Permission request \xB7 ${event.tool}`,
|
|
1314
|
+
body: `Requested approval for "${event.tool}".`,
|
|
1315
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function buildPermissionResolvedNotification(event) {
|
|
1320
|
+
return {
|
|
1321
|
+
type: "team-notification",
|
|
1322
|
+
teamId: event.teamId,
|
|
1323
|
+
notification: {
|
|
1324
|
+
id: `permission-resolved:${event.requestId}:${event.decision}`,
|
|
1325
|
+
teamId: event.teamId,
|
|
1326
|
+
memberId: event.memberId,
|
|
1327
|
+
kind: "permission-resolved",
|
|
1328
|
+
priority: "immediate",
|
|
1329
|
+
title: `Permission ${event.decision}`,
|
|
1330
|
+
body: event.decision === "allow" ? "Coordinator approved the pending tool request." : "Coordinator denied the pending tool request.",
|
|
1331
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
function buildShutdownRequestedNotification(event) {
|
|
1336
|
+
return {
|
|
1337
|
+
type: "team-notification",
|
|
1338
|
+
teamId: event.teamId,
|
|
1339
|
+
notification: {
|
|
1340
|
+
id: `shutdown-requested:${event.requestId}`,
|
|
1341
|
+
teamId: event.teamId,
|
|
1342
|
+
memberId: event.memberId,
|
|
1343
|
+
kind: "shutdown-requested",
|
|
1344
|
+
priority: "immediate",
|
|
1345
|
+
title: "Shutdown requested",
|
|
1346
|
+
body: event.reason ? `Shutdown requested. Reason: ${event.reason}` : "Shutdown requested.",
|
|
1347
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
function buildShutdownResolvedNotification(event) {
|
|
1352
|
+
return {
|
|
1353
|
+
type: "team-notification",
|
|
1354
|
+
teamId: event.teamId,
|
|
1355
|
+
notification: {
|
|
1356
|
+
id: `shutdown-resolved:${event.requestId}:${event.accepted ? "accepted" : "rejected"}`,
|
|
1357
|
+
teamId: event.teamId,
|
|
1358
|
+
memberId: event.memberId,
|
|
1359
|
+
kind: "shutdown-resolved",
|
|
1360
|
+
priority: "immediate",
|
|
1361
|
+
title: event.accepted ? "Shutdown accepted" : "Shutdown deferred",
|
|
1362
|
+
body: event.accepted ? "Member accepted shutdown and went offline." : "Member did not complete shutdown within the requested window.",
|
|
1363
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
async function buildTaskResultNotification(event, resolveTask) {
|
|
1368
|
+
const task = await resolveTask(event.taskId);
|
|
1369
|
+
const detail = event.type === "team-task-completed" ? event.result.response : event.type === "team-task-failed" ? event.error : event.reason ?? "Aborted";
|
|
1370
|
+
const status = event.type === "team-task-completed" ? "completed" : event.type === "team-task-failed" ? "failed" : "aborted";
|
|
1371
|
+
const priority = status === "completed" ? "deferred" : "immediate";
|
|
1372
|
+
const memberId = event.memberId ?? task?.memberId ?? "worker";
|
|
1373
|
+
return {
|
|
1374
|
+
type: "team-notification",
|
|
1375
|
+
teamId: event.teamId,
|
|
1376
|
+
notification: {
|
|
1377
|
+
id: `task-result:${event.taskId}:${status}:${task?.updatedAt ?? "unknown"}`,
|
|
1378
|
+
teamId: event.teamId,
|
|
1379
|
+
memberId,
|
|
1380
|
+
kind: "task-result",
|
|
1381
|
+
priority,
|
|
1382
|
+
title: task?.title ?? "Task result",
|
|
1383
|
+
body: formatTaskBody(task, status, detail),
|
|
1384
|
+
createdAt: task?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1385
|
+
taskId: event.taskId,
|
|
1386
|
+
status
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
async function buildCoordinatorNotificationEvent(event, resolveTask) {
|
|
1391
|
+
switch (event.type) {
|
|
1392
|
+
case "team-task-completed":
|
|
1393
|
+
case "team-task-failed":
|
|
1394
|
+
case "team-task-aborted":
|
|
1395
|
+
return buildTaskResultNotification(event, resolveTask);
|
|
1396
|
+
case "team-message":
|
|
1397
|
+
return buildWorkerReportNotification(event);
|
|
1398
|
+
case "team-permission-forwarded":
|
|
1399
|
+
return buildPermissionForwardedNotification(event);
|
|
1400
|
+
case "team-permission-resolved":
|
|
1401
|
+
return buildPermissionResolvedNotification(event);
|
|
1402
|
+
case "team-shutdown-requested":
|
|
1403
|
+
return buildShutdownRequestedNotification(event);
|
|
1404
|
+
case "team-shutdown-resolved":
|
|
1405
|
+
return buildShutdownResolvedNotification(event);
|
|
1406
|
+
default:
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// src/team/coordinator/inbox.ts
|
|
1412
|
+
var MAX_CONSECUTIVE_TURNS = 20;
|
|
1413
|
+
var MAX_AUTONOMOUS_TURNS = 30;
|
|
1414
|
+
function enqueueInboxItem(ctx, item) {
|
|
1415
|
+
ctx.coordinatorInbox.push(item);
|
|
1416
|
+
ctx.publishCoordinatorStatus({
|
|
1417
|
+
pendingInboxCount: ctx.coordinatorInbox.length
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
function enqueueNotification(ctx, notification) {
|
|
1421
|
+
if (notification.kind !== "worker-report" && notification.kind !== "task-result" && notification.kind !== "shutdown-requested" && notification.kind !== "shutdown-resolved") {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
enqueueInboxItem(ctx, {
|
|
1425
|
+
id: notification.id,
|
|
1426
|
+
kind: "notification",
|
|
1427
|
+
createdAt: notification.createdAt,
|
|
1428
|
+
body: notification.body,
|
|
1429
|
+
notification
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
function scheduleProcessing(ctx) {
|
|
1433
|
+
if (!ctx.started) {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (ctx.coordinatorProcessing) {
|
|
1437
|
+
ctx.coordinatorReschedule = true;
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
ctx.coordinatorProcessing = true;
|
|
1441
|
+
void processInbox(ctx).catch((err) => {
|
|
1442
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1443
|
+
ctx.log(`processInbox: unhandled error \u2014 ${msg}`);
|
|
1444
|
+
ctx.coordinatorProcessing = false;
|
|
1445
|
+
ctx.publishCoordinatorStatus({
|
|
1446
|
+
phase: "error",
|
|
1447
|
+
message: msg,
|
|
1448
|
+
activeTaskCount: 0
|
|
1449
|
+
});
|
|
1450
|
+
ctx.publishCoordinatorEvent({
|
|
1451
|
+
type: "error",
|
|
1452
|
+
error: err instanceof Error ? err : new Error(msg)
|
|
1453
|
+
});
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
async function processInbox(ctx) {
|
|
1457
|
+
try {
|
|
1458
|
+
const agent = ctx.ensureCoordinatorAgent();
|
|
1459
|
+
const sessionId = ctx.getCoordinatorSessionId();
|
|
1460
|
+
let consecutiveTurns = 0;
|
|
1461
|
+
ctx.log(`processInbox: starting drain (inbox=${ctx.coordinatorInbox.length}, processing=${ctx.coordinatorProcessing})`);
|
|
1462
|
+
while (ctx.started) {
|
|
1463
|
+
if (consecutiveTurns >= MAX_CONSECUTIVE_TURNS) {
|
|
1464
|
+
ctx.log(`Inbox processor yielding after ${consecutiveTurns} consecutive turns`);
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
const batch = ctx.coordinatorInbox.splice(0);
|
|
1468
|
+
ctx.publishCoordinatorStatus({
|
|
1469
|
+
pendingInboxCount: ctx.coordinatorInbox.length
|
|
1470
|
+
});
|
|
1471
|
+
if (batch.length === 0) {
|
|
1472
|
+
const activeTaskCount = await ctx.getActiveTaskCount();
|
|
1473
|
+
const nextPhase = activeTaskCount > 0 ? "waiting" : "ready";
|
|
1474
|
+
ctx.log(`processInbox: empty batch \u2192 phase=${nextPhase} (activeTasks=${activeTaskCount})`);
|
|
1475
|
+
ctx.publishCoordinatorStatus({
|
|
1476
|
+
phase: nextPhase,
|
|
1477
|
+
message: activeTaskCount > 0 ? `Waiting on ${activeTaskCount} task${activeTaskCount === 1 ? "" : "s"}` : "Ready",
|
|
1478
|
+
activeTaskCount
|
|
1479
|
+
});
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
ctx.log(`processInbox: batch of ${batch.length} items (${batch.map((i) => i.kind).join(", ")})`);
|
|
1483
|
+
const hasUser = batch.some((item) => item.kind === "user-message");
|
|
1484
|
+
const hasNotification = batch.some((item) => item.kind === "notification");
|
|
1485
|
+
let turnBatch;
|
|
1486
|
+
if (hasUser && hasNotification) {
|
|
1487
|
+
turnBatch = batch.filter((item) => item.kind === "user-message");
|
|
1488
|
+
const deferred = batch.filter((item) => item.kind !== "user-message");
|
|
1489
|
+
ctx.coordinatorInbox.unshift(...deferred);
|
|
1490
|
+
ctx.publishCoordinatorStatus({
|
|
1491
|
+
pendingInboxCount: ctx.coordinatorInbox.length
|
|
1492
|
+
});
|
|
1493
|
+
} else {
|
|
1494
|
+
turnBatch = batch;
|
|
1495
|
+
}
|
|
1496
|
+
if (hasUser) {
|
|
1497
|
+
ctx.autonomousTurnCount = 0;
|
|
1498
|
+
} else {
|
|
1499
|
+
ctx.autonomousTurnCount += 1;
|
|
1500
|
+
if (ctx.autonomousTurnCount > MAX_AUTONOMOUS_TURNS) {
|
|
1501
|
+
ctx.log(
|
|
1502
|
+
`Coordinator yielding after ${ctx.autonomousTurnCount} autonomous turns \u2014 waiting for user guidance`
|
|
1503
|
+
);
|
|
1504
|
+
ctx.coordinatorInbox.unshift(...turnBatch);
|
|
1505
|
+
const activeTaskCount = await ctx.getActiveTaskCount();
|
|
1506
|
+
ctx.publishCoordinatorStatus({
|
|
1507
|
+
phase: "ready",
|
|
1508
|
+
message: "Autonomous turn limit reached \u2014 send a message to continue",
|
|
1509
|
+
activeTaskCount,
|
|
1510
|
+
pendingInboxCount: ctx.coordinatorInbox.length
|
|
1511
|
+
});
|
|
1512
|
+
break;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
const firstUser = turnBatch.find((item) => item.kind === "user-message");
|
|
1516
|
+
const activeTaskCountBeforeTurn = await ctx.getActiveTaskCount();
|
|
1517
|
+
ctx.publishCoordinatorStatus({
|
|
1518
|
+
phase: "running",
|
|
1519
|
+
message: firstUser ? "Coordinator processing user guidance..." : "Coordinator processing team updates...",
|
|
1520
|
+
activeTaskCount: activeTaskCountBeforeTurn,
|
|
1521
|
+
pendingInboxCount: ctx.coordinatorInbox.length
|
|
1522
|
+
});
|
|
1523
|
+
const message = formatCoordinatorInboxBatch(turnBatch);
|
|
1524
|
+
ctx.log(`processInbox: running coordinator turn #${consecutiveTurns + 1} (autonomousTurns=${ctx.autonomousTurnCount})`);
|
|
1525
|
+
let turnAssignedCount = 0;
|
|
1526
|
+
let turnResponseLength = 0;
|
|
1527
|
+
try {
|
|
1528
|
+
const turn = await runCoordinatorTurn(
|
|
1529
|
+
// The turn runner expects the full TeamCoordinator for turn
|
|
1530
|
+
// state management. The InboxContext is a superset at runtime
|
|
1531
|
+
// since the coordinator implements it.
|
|
1532
|
+
ctx,
|
|
1533
|
+
agent,
|
|
1534
|
+
sessionId,
|
|
1535
|
+
message,
|
|
1536
|
+
{
|
|
1537
|
+
onEvent: (event) => ctx.publishCoordinatorEvent(event),
|
|
1538
|
+
onStatus: (status) => ctx.publishCoordinatorStatus(status),
|
|
1539
|
+
onLog: (msg) => ctx.log(msg)
|
|
1540
|
+
}
|
|
1541
|
+
);
|
|
1542
|
+
const completionEntries = [];
|
|
1543
|
+
for (const item of turnBatch) {
|
|
1544
|
+
const taskId = item.notification?.kind === "task-result" ? item.notification.taskId : void 0;
|
|
1545
|
+
if (!taskId) {
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
const task = await ctx.getTask(taskId);
|
|
1549
|
+
if (!task) {
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
completionEntries.push({
|
|
1553
|
+
task,
|
|
1554
|
+
result: task.result
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
const reportEntries = turnBatch.filter((item) => item.notification?.kind === "worker-report").map((item) => ({
|
|
1558
|
+
messageId: item.notification?.id ?? item.id,
|
|
1559
|
+
memberId: item.notification?.memberId ?? "worker",
|
|
1560
|
+
taskId: item.notification?.taskId,
|
|
1561
|
+
createdAt: item.notification?.createdAt ?? item.createdAt,
|
|
1562
|
+
message: item.notification?.body ?? item.body
|
|
1563
|
+
}));
|
|
1564
|
+
ctx.coordinatorRoundNumber += 1;
|
|
1565
|
+
await ctx.emit({
|
|
1566
|
+
type: "team-coordinator-round",
|
|
1567
|
+
teamId: ctx.teamId,
|
|
1568
|
+
roundNumber: ctx.coordinatorRoundNumber,
|
|
1569
|
+
assigned: turn.actions.assigned,
|
|
1570
|
+
messaged: turn.actions.sentMessages,
|
|
1571
|
+
stopped: turn.actions.abortedTaskIds,
|
|
1572
|
+
completions: completionEntries,
|
|
1573
|
+
reports: reportEntries,
|
|
1574
|
+
response: turn.response,
|
|
1575
|
+
usage: turn.usage
|
|
1576
|
+
});
|
|
1577
|
+
ctx.publishCoordinatorEvent({
|
|
1578
|
+
type: "complete",
|
|
1579
|
+
output: turn.response,
|
|
1580
|
+
usage: turn.usage
|
|
1581
|
+
});
|
|
1582
|
+
turnAssignedCount = turn.actions.assigned.length;
|
|
1583
|
+
turnResponseLength = turn.response.length;
|
|
1584
|
+
consecutiveTurns += 1;
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
1587
|
+
ctx.log(`processInbox: turn error \u2014 ${messageText}`);
|
|
1588
|
+
ctx.publishCoordinatorStatus({
|
|
1589
|
+
phase: "error",
|
|
1590
|
+
message: messageText,
|
|
1591
|
+
activeTaskCount: await ctx.getActiveTaskCount()
|
|
1592
|
+
});
|
|
1593
|
+
ctx.publishCoordinatorEvent({
|
|
1594
|
+
type: "error",
|
|
1595
|
+
error: error instanceof Error ? error : new Error(messageText)
|
|
1596
|
+
});
|
|
1597
|
+
break;
|
|
1598
|
+
}
|
|
1599
|
+
const activeTaskCountAfterTurn = await ctx.getActiveTaskCount();
|
|
1600
|
+
const afterPhase = activeTaskCountAfterTurn > 0 ? "waiting" : "ready";
|
|
1601
|
+
ctx.log(`processInbox: turn done \u2014 assigned=${turnAssignedCount} response=${turnResponseLength > 0 ? turnResponseLength + "ch" : "(empty)"} \u2192 phase=${afterPhase} (activeTasks=${activeTaskCountAfterTurn}, inbox=${ctx.coordinatorInbox.length})`);
|
|
1602
|
+
ctx.publishCoordinatorStatus({
|
|
1603
|
+
phase: afterPhase,
|
|
1604
|
+
message: activeTaskCountAfterTurn > 0 ? `Waiting on ${activeTaskCountAfterTurn} task${activeTaskCountAfterTurn === 1 ? "" : "s"}` : "Ready",
|
|
1605
|
+
activeTaskCount: activeTaskCountAfterTurn,
|
|
1606
|
+
pendingInboxCount: ctx.coordinatorInbox.length
|
|
1607
|
+
});
|
|
1608
|
+
if (ctx.coordinatorInbox.length === 0 && activeTaskCountAfterTurn > 0) {
|
|
1609
|
+
ctx.log(`processInbox: yielding \u2014 waiting on ${activeTaskCountAfterTurn} task(s)`);
|
|
1610
|
+
break;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
} finally {
|
|
1614
|
+
ctx.coordinatorProcessing = false;
|
|
1615
|
+
if (ctx.coordinatorReschedule || ctx.coordinatorInbox.length > 0) {
|
|
1616
|
+
ctx.log(`processInbox: rescheduling (reschedule=${ctx.coordinatorReschedule}, inbox=${ctx.coordinatorInbox.length})`);
|
|
1617
|
+
ctx.coordinatorReschedule = false;
|
|
1618
|
+
ctx.scheduleCoordinatorProcessing();
|
|
1619
|
+
} else {
|
|
1620
|
+
ctx.log(`processInbox: idle \u2014 resolving waiters`);
|
|
1621
|
+
ctx.resolveCoordinatorIdleWaiters();
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/team/coordinator/planning.ts
|
|
1627
|
+
async function planTasks(coordinator, prompt, memberIds) {
|
|
1628
|
+
const targets = memberIds ? memberIds.map((id) => {
|
|
1629
|
+
const rt = coordinator.members.get(id);
|
|
1630
|
+
if (!rt) {
|
|
1631
|
+
throw new Error(
|
|
1632
|
+
`Unknown member: "${id}". Registered: ${[...coordinator.members.keys()].join(", ")}`
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
return rt;
|
|
1636
|
+
}) : [...coordinator.members.values()];
|
|
1637
|
+
if (targets.length === 0) {
|
|
1638
|
+
throw new Error("No members available for planning");
|
|
1639
|
+
}
|
|
1640
|
+
const memberDescriptions = targets.map(
|
|
1641
|
+
(rt) => `- ${rt.member.id} (${rt.role.name}): ${rt.role.description ?? "General purpose"}`
|
|
1642
|
+
).join("\n");
|
|
1643
|
+
const planningPrompt = "You are the team lead. Decompose the following task into targeted sub-tasks for your teammates.\n\nAvailable teammates:\n" + memberDescriptions + "\n\nUser request:\n" + prompt + `
|
|
1644
|
+
|
|
1645
|
+
Create a focused, self-contained task prompt for each relevant teammate. Each prompt must include all necessary context \u2014 teammates cannot see the original request or each other's tasks.
|
|
1646
|
+
|
|
1647
|
+
Respond with ONLY a JSON array (no markdown fences, no explanation):
|
|
1648
|
+
[
|
|
1649
|
+
{ "memberId": "id", "title": "short title", "prompt": "detailed self-contained prompt" }
|
|
1650
|
+
]
|
|
1651
|
+
|
|
1652
|
+
You may omit teammates that are not needed for this task.`;
|
|
1653
|
+
const result = await coordinator.lead.send(coordinator.leadSessionId, planningPrompt);
|
|
1654
|
+
const tasks = parsePlanResponse(result.response, prompt, targets);
|
|
1655
|
+
await coordinator.emit({
|
|
1656
|
+
type: "team-plan-created",
|
|
1657
|
+
teamId: coordinator.teamId,
|
|
1658
|
+
tasks
|
|
1659
|
+
});
|
|
1660
|
+
return { tasks, usage: result.usage };
|
|
1661
|
+
}
|
|
1662
|
+
async function planAndExecuteTasks(coordinator, prompt, memberIds) {
|
|
1663
|
+
const teamPlan = await planTasks(coordinator, prompt, memberIds);
|
|
1664
|
+
const planIndexToTaskId = /* @__PURE__ */ new Map();
|
|
1665
|
+
const tasks = [];
|
|
1666
|
+
for (let i = 0; i < teamPlan.tasks.length; i++) {
|
|
1667
|
+
const planned = teamPlan.tasks[i];
|
|
1668
|
+
let resolvedDeps;
|
|
1669
|
+
if (planned.dependsOn && planned.dependsOn.length > 0) {
|
|
1670
|
+
resolvedDeps = [];
|
|
1671
|
+
for (const dep of planned.dependsOn) {
|
|
1672
|
+
const idx = Number(dep);
|
|
1673
|
+
if (!Number.isNaN(idx) && planIndexToTaskId.has(idx)) {
|
|
1674
|
+
resolvedDeps.push(planIndexToTaskId.get(idx));
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
const match = tasks.find(
|
|
1678
|
+
(t) => t.title === dep || t.memberId === dep || t.id === dep
|
|
1679
|
+
);
|
|
1680
|
+
if (match) {
|
|
1681
|
+
resolvedDeps.push(match.id);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
if (resolvedDeps.length === 0) resolvedDeps = void 0;
|
|
1685
|
+
}
|
|
1686
|
+
const task = await coordinator.queue({
|
|
1687
|
+
memberId: planned.memberId,
|
|
1688
|
+
title: planned.title,
|
|
1689
|
+
prompt: planned.prompt,
|
|
1690
|
+
dependsOn: resolvedDeps
|
|
1691
|
+
});
|
|
1692
|
+
tasks.push(task);
|
|
1693
|
+
planIndexToTaskId.set(i, task.id);
|
|
1694
|
+
}
|
|
1695
|
+
return { plan: teamPlan, tasks };
|
|
1696
|
+
}
|
|
1697
|
+
function parsePlanResponse(response, originalPrompt, targets) {
|
|
1698
|
+
const validIds = new Set(targets.map((t) => t.member.id));
|
|
1699
|
+
let jsonStr = response.trim();
|
|
1700
|
+
const codeBlockMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
1701
|
+
if (codeBlockMatch) {
|
|
1702
|
+
jsonStr = codeBlockMatch[1].trim();
|
|
1703
|
+
}
|
|
1704
|
+
const arrayMatch = jsonStr.match(/\[[\s\S]*\]/);
|
|
1705
|
+
if (arrayMatch) {
|
|
1706
|
+
jsonStr = arrayMatch[0];
|
|
1707
|
+
}
|
|
1708
|
+
let parsed;
|
|
1709
|
+
try {
|
|
1710
|
+
parsed = JSON.parse(jsonStr);
|
|
1711
|
+
} catch {
|
|
1712
|
+
return targets.map((rt) => ({
|
|
1713
|
+
memberId: rt.member.id,
|
|
1714
|
+
title: `task:${rt.role.name}`,
|
|
1715
|
+
prompt: originalPrompt
|
|
1716
|
+
}));
|
|
1717
|
+
}
|
|
1718
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
1719
|
+
return targets.map((rt) => ({
|
|
1720
|
+
memberId: rt.member.id,
|
|
1721
|
+
title: `task:${rt.role.name}`,
|
|
1722
|
+
prompt: originalPrompt
|
|
1723
|
+
}));
|
|
1724
|
+
}
|
|
1725
|
+
const tasks = [];
|
|
1726
|
+
for (const item of parsed) {
|
|
1727
|
+
if (typeof item !== "object" || item === null) continue;
|
|
1728
|
+
const obj = item;
|
|
1729
|
+
const memberId = obj.memberId;
|
|
1730
|
+
const prompt = obj.prompt;
|
|
1731
|
+
if (typeof memberId !== "string" || typeof prompt !== "string") continue;
|
|
1732
|
+
if (!validIds.has(memberId)) continue;
|
|
1733
|
+
tasks.push({
|
|
1734
|
+
memberId,
|
|
1735
|
+
title: typeof obj.title === "string" ? obj.title : `task:${memberId}`,
|
|
1736
|
+
prompt,
|
|
1737
|
+
dependsOn: Array.isArray(obj.dependsOn) ? obj.dependsOn.filter((d) => typeof d === "string") : void 0
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
if (tasks.length === 0) {
|
|
1741
|
+
return targets.map((rt) => ({
|
|
1742
|
+
memberId: rt.member.id,
|
|
1743
|
+
title: `task:${rt.role.name}`,
|
|
1744
|
+
prompt: originalPrompt
|
|
1745
|
+
}));
|
|
1746
|
+
}
|
|
1747
|
+
return tasks;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/team/coordinator/synthesis.ts
|
|
1751
|
+
async function synthesizeResults(coordinator, originalPrompt, taskIds) {
|
|
1752
|
+
const allTasks = await coordinator.taskBoard.listTasks();
|
|
1753
|
+
const terminal = (taskIds ? allTasks.filter((t) => taskIds.includes(t.id)) : allTasks).filter(
|
|
1754
|
+
(t) => t.status === "completed" || t.status === "aborted" || t.status === "failed" || t.status === "cancelled"
|
|
1755
|
+
);
|
|
1756
|
+
if (terminal.length === 0) {
|
|
1757
|
+
throw new Error("No completed tasks to synthesize");
|
|
1758
|
+
}
|
|
1759
|
+
const sections = terminal.map((task) => {
|
|
1760
|
+
const runtime = coordinator.members.get(task.memberId ?? "");
|
|
1761
|
+
const name = runtime?.role.name ?? task.memberId ?? "unassigned";
|
|
1762
|
+
if (task.status === "completed" && task.result) {
|
|
1763
|
+
return `## ${name}
|
|
1764
|
+
${task.result.response}`;
|
|
1765
|
+
}
|
|
1766
|
+
return `## ${name}
|
|
1767
|
+
Status: ${task.status}
|
|
1768
|
+
${task.error ?? "No result."}`;
|
|
1769
|
+
});
|
|
1770
|
+
const synthesisPrompt = `You are the team lead. Synthesize the findings from your teammates into one clear, comprehensive answer.
|
|
1771
|
+
|
|
1772
|
+
Original task:
|
|
1773
|
+
${originalPrompt}
|
|
1774
|
+
|
|
1775
|
+
Teammate results:
|
|
1776
|
+
|
|
1777
|
+
` + sections.join("\n\n");
|
|
1778
|
+
const result = await coordinator.lead.send(coordinator.leadSessionId, synthesisPrompt);
|
|
1779
|
+
await coordinator.emit({
|
|
1780
|
+
type: "team-synthesis",
|
|
1781
|
+
teamId: coordinator.teamId,
|
|
1782
|
+
response: result.response
|
|
1783
|
+
});
|
|
1784
|
+
return result;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// src/team/coordinator/coordinator.ts
|
|
1788
|
+
var TeamCoordinator = class {
|
|
1789
|
+
teamId;
|
|
1790
|
+
/** @internal */
|
|
1791
|
+
lead;
|
|
1792
|
+
/** @internal */
|
|
1793
|
+
leadSessionId;
|
|
1794
|
+
/** @internal */
|
|
1795
|
+
createId;
|
|
1796
|
+
roles = /* @__PURE__ */ new Map();
|
|
1797
|
+
/** @internal */
|
|
1798
|
+
members = /* @__PURE__ */ new Map();
|
|
1799
|
+
/** @internal */
|
|
1800
|
+
taskBoard;
|
|
1801
|
+
/** @internal */
|
|
1802
|
+
mailbox;
|
|
1803
|
+
onEvent;
|
|
1804
|
+
eventListeners = /* @__PURE__ */ new Set();
|
|
1805
|
+
signal;
|
|
1806
|
+
/** @internal */
|
|
1807
|
+
workLoopEnabled;
|
|
1808
|
+
/** @internal */
|
|
1809
|
+
taskDispatchMode;
|
|
1810
|
+
/** @internal */
|
|
1811
|
+
pollIntervalMs;
|
|
1812
|
+
/** @internal */
|
|
1813
|
+
onPermissionRequest;
|
|
1814
|
+
/** @internal */
|
|
1815
|
+
beforeIteration;
|
|
1816
|
+
/** @internal */
|
|
1817
|
+
sharedPermissions;
|
|
1818
|
+
/** @internal */
|
|
1819
|
+
taskExecutor;
|
|
1820
|
+
externalTaskControl;
|
|
1821
|
+
/** @internal */
|
|
1822
|
+
started = false;
|
|
1823
|
+
onLog;
|
|
1824
|
+
logger;
|
|
1825
|
+
activeCoordinatorTurn;
|
|
1826
|
+
mailboxUnsubscribe;
|
|
1827
|
+
coordinatorAgent;
|
|
1828
|
+
coordinatorSessionId;
|
|
1829
|
+
/** @internal */
|
|
1830
|
+
coordinatorInbox = [];
|
|
1831
|
+
/** @internal */
|
|
1832
|
+
coordinatorProcessing = false;
|
|
1833
|
+
/** @internal */
|
|
1834
|
+
coordinatorReschedule = false;
|
|
1835
|
+
coordinatorStatus = {
|
|
1836
|
+
phase: "ready",
|
|
1837
|
+
message: "Ready",
|
|
1838
|
+
pendingInboxCount: 0,
|
|
1839
|
+
activeTaskCount: 0
|
|
1840
|
+
};
|
|
1841
|
+
coordinatorEventListeners = /* @__PURE__ */ new Set();
|
|
1842
|
+
coordinatorStatusListeners = /* @__PURE__ */ new Set();
|
|
1843
|
+
coordinatorIdleResolvers = [];
|
|
1844
|
+
/** @internal */
|
|
1845
|
+
coordinatorRoundNumber = 0;
|
|
1846
|
+
coordinatorRunSequence = 0;
|
|
1847
|
+
coordinatorTurnState = null;
|
|
1848
|
+
/** @internal Structured log output — writes to both logger and onLog when provided. */
|
|
1849
|
+
log(msg) {
|
|
1850
|
+
const tagged = `[team] ${msg}`;
|
|
1851
|
+
this.logger?.info(tagged);
|
|
1852
|
+
this.onLog?.(tagged);
|
|
1853
|
+
}
|
|
1854
|
+
async dispatchEvent(event) {
|
|
1855
|
+
if (this.signal) {
|
|
1856
|
+
try {
|
|
1857
|
+
this.signal.emit(event);
|
|
1858
|
+
} catch {
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
for (const listener of this.eventListeners) {
|
|
1862
|
+
try {
|
|
1863
|
+
await listener(event);
|
|
1864
|
+
} catch {
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if (this.onEvent) {
|
|
1868
|
+
try {
|
|
1869
|
+
await this.onEvent(event);
|
|
1870
|
+
} catch {
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
subscribeTeamEvents(listener) {
|
|
1875
|
+
this.eventListeners.add(listener);
|
|
1876
|
+
return () => {
|
|
1877
|
+
this.eventListeners.delete(listener);
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
/** @internal */
|
|
1881
|
+
publishCoordinatorEvent(event) {
|
|
1882
|
+
for (const listener of this.coordinatorEventListeners) {
|
|
1883
|
+
try {
|
|
1884
|
+
listener(event);
|
|
1885
|
+
} catch {
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
/** @internal */
|
|
1890
|
+
publishCoordinatorStatus(status) {
|
|
1891
|
+
this.coordinatorStatus = {
|
|
1892
|
+
...this.coordinatorStatus,
|
|
1893
|
+
...status,
|
|
1894
|
+
pendingInboxCount: status.pendingInboxCount ?? this.coordinatorInbox.length
|
|
1895
|
+
};
|
|
1896
|
+
for (const listener of this.coordinatorStatusListeners) {
|
|
1897
|
+
try {
|
|
1898
|
+
listener({ ...this.coordinatorStatus });
|
|
1899
|
+
} catch {
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
/** @internal */
|
|
1904
|
+
resolveCoordinatorIdleWaiters() {
|
|
1905
|
+
if (this.coordinatorProcessing) {
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
if (this.coordinatorInbox.length > 0) {
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
if (this.coordinatorStatus.phase !== "ready" && this.coordinatorStatus.phase !== "error") {
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
const resolvers = this.coordinatorIdleResolvers.splice(0);
|
|
1915
|
+
for (const resolve of resolvers) {
|
|
1916
|
+
resolve();
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
constructor(config) {
|
|
1920
|
+
this.lead = config.lead;
|
|
1921
|
+
this.leadSessionId = config.leadSessionId;
|
|
1922
|
+
this.teamId = config.teamId?.trim() || `team-${randomUUID2().slice(0, 8)}`;
|
|
1923
|
+
this.createId = config.createId ?? (() => randomUUID2());
|
|
1924
|
+
this.onEvent = config.onEvent;
|
|
1925
|
+
this.signal = config.signal;
|
|
1926
|
+
this.taskDispatchMode = config.taskDispatchMode ?? (config.workLoopEnabled ?? false ? "workloop" : "manual");
|
|
1927
|
+
this.workLoopEnabled = this.taskDispatchMode === "workloop";
|
|
1928
|
+
this.pollIntervalMs = config.pollIntervalMs ?? 500;
|
|
1929
|
+
this.onPermissionRequest = config.onPermissionRequest;
|
|
1930
|
+
this.beforeIteration = config.beforeIteration;
|
|
1931
|
+
this.sharedPermissions = config.sharedPermissions;
|
|
1932
|
+
this.taskExecutor = config.taskExecutor;
|
|
1933
|
+
this.externalTaskControl = config.externalTaskControl;
|
|
1934
|
+
this.onLog = config.onLog;
|
|
1935
|
+
this.logger = config.logger?.child("team");
|
|
1936
|
+
this.taskBoard = new TaskBoard(
|
|
1937
|
+
config.taskBoardStore ?? new InMemoryTaskBoardStore()
|
|
1938
|
+
);
|
|
1939
|
+
this.mailbox = new Mailbox({
|
|
1940
|
+
store: config.mailboxStore ?? new InMemoryMailboxStore(),
|
|
1941
|
+
createId: this.createId
|
|
1942
|
+
});
|
|
1943
|
+
for (const role of config.roles) {
|
|
1944
|
+
this.roles.set(role.name, { ...role });
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
1948
|
+
async start() {
|
|
1949
|
+
if (this.started) return;
|
|
1950
|
+
await this.taskBoard.start();
|
|
1951
|
+
try {
|
|
1952
|
+
await this.mailbox.start();
|
|
1953
|
+
} catch (error) {
|
|
1954
|
+
await this.taskBoard.stop().catch(() => {
|
|
1955
|
+
});
|
|
1956
|
+
throw error;
|
|
1957
|
+
}
|
|
1958
|
+
this.started = true;
|
|
1959
|
+
this.mailboxUnsubscribe = this.mailbox.subscribe((message) => {
|
|
1960
|
+
if (message.teamId !== this.teamId) {
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
if (message.to !== "coordinator") {
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
if (message.payload?.kind !== "worker-report") {
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
void this.emit({
|
|
1970
|
+
type: "team-message",
|
|
1971
|
+
teamId: this.teamId,
|
|
1972
|
+
message
|
|
1973
|
+
});
|
|
1974
|
+
});
|
|
1975
|
+
this.log(`Started (${this.members.size} member(s), ${this.roles.size} role(s))`);
|
|
1976
|
+
this.publishCoordinatorStatus({
|
|
1977
|
+
phase: "ready",
|
|
1978
|
+
message: "Ready",
|
|
1979
|
+
pendingInboxCount: 0,
|
|
1980
|
+
activeTaskCount: 0
|
|
1981
|
+
});
|
|
1982
|
+
if (this.workLoopEnabled) {
|
|
1983
|
+
for (const runtime of this.members.values()) {
|
|
1984
|
+
if (!runtime.workLoop) {
|
|
1985
|
+
runtime.stopRequested = false;
|
|
1986
|
+
runtime.workLoop = runWorkLoop(this, runtime).catch((err) => {
|
|
1987
|
+
this.log(`[${runtime.member.id}] work-loop crashed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
await this.emit({
|
|
1993
|
+
type: "team-started",
|
|
1994
|
+
teamId: this.teamId,
|
|
1995
|
+
memberCount: this.members.size
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
async stop() {
|
|
1999
|
+
if (!this.started) return;
|
|
2000
|
+
this.started = false;
|
|
2001
|
+
this.log("Stopping...");
|
|
2002
|
+
this.mailboxUnsubscribe?.();
|
|
2003
|
+
this.mailboxUnsubscribe = void 0;
|
|
2004
|
+
this.detachCoordinatorTurn();
|
|
2005
|
+
this.coordinatorInbox = [];
|
|
2006
|
+
this.coordinatorReschedule = false;
|
|
2007
|
+
this.coordinatorProcessing = false;
|
|
2008
|
+
this.autonomousTurnCount = 0;
|
|
2009
|
+
this.endCoordinatorTurnState();
|
|
2010
|
+
for (const runtime of this.members.values()) {
|
|
2011
|
+
runtime.stopRequested = true;
|
|
2012
|
+
}
|
|
2013
|
+
const promises = [];
|
|
2014
|
+
for (const m of this.members.values()) {
|
|
2015
|
+
if (m.workLoop) promises.push(m.workLoop);
|
|
2016
|
+
if (m.activeRun) promises.push(m.activeRun);
|
|
2017
|
+
}
|
|
2018
|
+
await Promise.allSettled(promises);
|
|
2019
|
+
for (const runtime of this.members.values()) {
|
|
2020
|
+
runtime.workLoop = void 0;
|
|
2021
|
+
}
|
|
2022
|
+
let firstError;
|
|
2023
|
+
try {
|
|
2024
|
+
await this.mailbox.stop();
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
firstError = error;
|
|
2027
|
+
}
|
|
2028
|
+
try {
|
|
2029
|
+
await this.taskBoard.stop();
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
if (!firstError) {
|
|
2032
|
+
firstError = error;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
await this.emit({
|
|
2036
|
+
type: "team-stopped",
|
|
2037
|
+
teamId: this.teamId
|
|
2038
|
+
});
|
|
2039
|
+
this.publishCoordinatorStatus({
|
|
2040
|
+
phase: "ready",
|
|
2041
|
+
message: "Stopped",
|
|
2042
|
+
pendingInboxCount: 0,
|
|
2043
|
+
activeTaskCount: 0
|
|
2044
|
+
});
|
|
2045
|
+
this.resolveCoordinatorIdleWaiters();
|
|
2046
|
+
if (firstError) {
|
|
2047
|
+
throw firstError;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
// ── Member registration ────────────────────────────────────────────
|
|
2051
|
+
/**
|
|
2052
|
+
* Register a team member from a role.
|
|
2053
|
+
* Forks the lead agent with the role's specialization.
|
|
2054
|
+
*/
|
|
2055
|
+
async register(roleName, memberId = roleName) {
|
|
2056
|
+
const id = memberId.trim();
|
|
2057
|
+
if (!id) throw new Error("Member ID must not be empty");
|
|
2058
|
+
const existing = this.members.get(id);
|
|
2059
|
+
if (existing) return { ...existing.member };
|
|
2060
|
+
const role = this.roles.get(roleName);
|
|
2061
|
+
if (!role) {
|
|
2062
|
+
throw new Error(
|
|
2063
|
+
`Unknown role: "${roleName}". Available: ${[...this.roles.keys()].join(", ")}`
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
const permissionRules = (this.sharedPermissions ?? []).map((rule) => ({
|
|
2067
|
+
tool: rule.tool,
|
|
2068
|
+
pattern: rule.pattern,
|
|
2069
|
+
action: "allow"
|
|
2070
|
+
}));
|
|
2071
|
+
const inheritedMiddleware = [...this.lead.getMiddlewareRunner().getMiddleware()];
|
|
2072
|
+
const inheritedApprovalMiddleware = inheritedMiddleware.filter((mw) => mw.name === "approval");
|
|
2073
|
+
const needsTeamPermissionPolicy = permissionRules.length > 0 || inheritedApprovalMiddleware.length > 0 || Boolean(this.onPermissionRequest);
|
|
2074
|
+
const memberMiddleware = needsTeamPermissionPolicy ? [
|
|
2075
|
+
...inheritedMiddleware.filter(
|
|
2076
|
+
(mw) => mw.name !== "approval" && !mw.name.startsWith("team-permission:")
|
|
2077
|
+
),
|
|
2078
|
+
...role.additionalMiddleware ?? [],
|
|
2079
|
+
teamPermissionPolicy({
|
|
2080
|
+
memberId: id,
|
|
2081
|
+
rules: permissionRules,
|
|
2082
|
+
forwardApproval: async (_memberId, tool, args, ctx) => {
|
|
2083
|
+
if (this.onPermissionRequest) {
|
|
2084
|
+
return this.createPermissionForwarder(id)(_memberId, tool, args, ctx);
|
|
2085
|
+
}
|
|
2086
|
+
let currentArgs = args;
|
|
2087
|
+
for (const approval of inheritedApprovalMiddleware) {
|
|
2088
|
+
if (!approval.beforeToolCall) continue;
|
|
2089
|
+
const decision = await approval.beforeToolCall(tool, currentArgs, ctx);
|
|
2090
|
+
if (decision.action === "deny") {
|
|
2091
|
+
return decision;
|
|
2092
|
+
}
|
|
2093
|
+
if (decision.args !== void 0) {
|
|
2094
|
+
currentArgs = decision.args;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return currentArgs === args ? { action: "allow" } : { action: "allow", args: currentArgs };
|
|
2098
|
+
}
|
|
2099
|
+
})
|
|
2100
|
+
] : void 0;
|
|
2101
|
+
const agent = this.lead.fork({
|
|
2102
|
+
name: id,
|
|
2103
|
+
profile: role.profile,
|
|
2104
|
+
systemPrompt: role.systemPrompt,
|
|
2105
|
+
model: role.model,
|
|
2106
|
+
maxSteps: role.maxSteps,
|
|
2107
|
+
...memberMiddleware ? { middleware: memberMiddleware } : { additionalMiddleware: role.additionalMiddleware }
|
|
2108
|
+
});
|
|
2109
|
+
for (const tool of role.additionalTools ?? []) {
|
|
2110
|
+
agent.addTool(tool);
|
|
2111
|
+
}
|
|
2112
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2113
|
+
const sessionId = `team:${this.teamId}:${id}`;
|
|
2114
|
+
const member = {
|
|
2115
|
+
id,
|
|
2116
|
+
role: role.name,
|
|
2117
|
+
description: role.description,
|
|
2118
|
+
sessionId,
|
|
2119
|
+
status: "idle",
|
|
2120
|
+
createdAt: ts,
|
|
2121
|
+
updatedAt: ts
|
|
2122
|
+
};
|
|
2123
|
+
await this.taskBoard.putMember(member);
|
|
2124
|
+
const stats = {
|
|
2125
|
+
tasksCompleted: 0,
|
|
2126
|
+
tasksFailed: 0,
|
|
2127
|
+
totalTokens: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
2128
|
+
totalToolCalls: 0,
|
|
2129
|
+
totalDurationMs: 0,
|
|
2130
|
+
lastActivityAt: ts
|
|
2131
|
+
};
|
|
2132
|
+
this.members.set(id, {
|
|
2133
|
+
member,
|
|
2134
|
+
role,
|
|
2135
|
+
agent,
|
|
2136
|
+
stopRequested: false,
|
|
2137
|
+
stats
|
|
2138
|
+
});
|
|
2139
|
+
this.log(`Registered member @${id} (role: ${role.name}${role.description ? `, ${role.description}` : ""})`);
|
|
2140
|
+
await this.emit({
|
|
2141
|
+
type: "team-member-registered",
|
|
2142
|
+
teamId: this.teamId,
|
|
2143
|
+
member: { ...member }
|
|
2144
|
+
});
|
|
2145
|
+
const runtime = this.members.get(id);
|
|
2146
|
+
agent.addTool(createAskCoordinatorTool({
|
|
2147
|
+
memberId: id,
|
|
2148
|
+
teamId: this.teamId,
|
|
2149
|
+
runtime,
|
|
2150
|
+
mailbox: this.mailbox
|
|
2151
|
+
}));
|
|
2152
|
+
if (this.taskDispatchMode === "workloop" && this.started) {
|
|
2153
|
+
runtime.workLoop = runWorkLoop(this, runtime).catch((err) => {
|
|
2154
|
+
this.log(`[${id}] work-loop crashed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
return { ...member };
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Register all roles (or a subset) as team members.
|
|
2161
|
+
*/
|
|
2162
|
+
async registerAll(roleNames) {
|
|
2163
|
+
const names = roleNames?.length ? roleNames : [...this.roles.keys()];
|
|
2164
|
+
return Promise.all(names.map((name) => this.register(name)));
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* List available role names.
|
|
2168
|
+
*/
|
|
2169
|
+
listRoles() {
|
|
2170
|
+
return [...this.roles.keys()];
|
|
2171
|
+
}
|
|
2172
|
+
// ── Task management ────────────────────────────────────────────────
|
|
2173
|
+
/**
|
|
2174
|
+
* Queue a task. If dependencies are met and a member is idle,
|
|
2175
|
+
* the task is dispatched immediately.
|
|
2176
|
+
*/
|
|
2177
|
+
async queue(input) {
|
|
2178
|
+
if (input.memberId) {
|
|
2179
|
+
this.requireMember(input.memberId);
|
|
2180
|
+
}
|
|
2181
|
+
const { task, transitions } = await this.taskBoard.createTask({
|
|
2182
|
+
id: this.createId(),
|
|
2183
|
+
memberId: input.memberId,
|
|
2184
|
+
title: input.title,
|
|
2185
|
+
prompt: input.prompt,
|
|
2186
|
+
dependsOn: input.dependsOn
|
|
2187
|
+
});
|
|
2188
|
+
this.log(`Queued task "${input.title}" \u2192 ${input.memberId ?? "unassigned"}${input.dependsOn?.length ? ` (depends on ${input.dependsOn.length} task(s))` : ""}`);
|
|
2189
|
+
await this.emitTransitions(transitions);
|
|
2190
|
+
if (this.taskDispatchMode === "manual") {
|
|
2191
|
+
await this.dispatchReady();
|
|
2192
|
+
}
|
|
2193
|
+
return await this.taskBoard.getTask(task.id) ?? task;
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Dispatch all pending tasks to idle members.
|
|
2197
|
+
* Called automatically after queue/complete/fail/cancel.
|
|
2198
|
+
*/
|
|
2199
|
+
async dispatchReady() {
|
|
2200
|
+
if (this.taskDispatchMode === "external") {
|
|
2201
|
+
return [];
|
|
2202
|
+
}
|
|
2203
|
+
const dispatched = [];
|
|
2204
|
+
const members = await this.taskBoard.listMembers();
|
|
2205
|
+
for (const member of members) {
|
|
2206
|
+
if (member.status !== "idle") continue;
|
|
2207
|
+
const runtime = this.members.get(member.id);
|
|
2208
|
+
if (!runtime) continue;
|
|
2209
|
+
const task = await claimAndExecute(this, runtime);
|
|
2210
|
+
if (task) dispatched.push(task);
|
|
2211
|
+
}
|
|
2212
|
+
return dispatched;
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Wait for a specific task to reach a terminal state.
|
|
2216
|
+
*/
|
|
2217
|
+
async waitForTask(taskId, timeoutMs) {
|
|
2218
|
+
const deadline = timeoutMs !== void 0 ? Date.now() + timeoutMs : void 0;
|
|
2219
|
+
while (true) {
|
|
2220
|
+
const task = await this.taskBoard.getTask(taskId);
|
|
2221
|
+
if (!task) throw new Error(`Unknown task: ${taskId}`);
|
|
2222
|
+
if (task.status === "completed" || task.status === "aborted" || task.status === "failed" || task.status === "cancelled") {
|
|
2223
|
+
return { task, result: task.result };
|
|
2224
|
+
}
|
|
2225
|
+
if (deadline && Date.now() >= deadline) {
|
|
2226
|
+
throw new Error(`Timed out waiting for task "${taskId}"`);
|
|
2227
|
+
}
|
|
2228
|
+
await sleep(100);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Wait for all tasks to reach terminal states.
|
|
2233
|
+
*/
|
|
2234
|
+
async waitForAll(timeoutMs) {
|
|
2235
|
+
const deadline = timeoutMs !== void 0 ? Date.now() + timeoutMs : void 0;
|
|
2236
|
+
const tasks = await this.taskBoard.listTasks();
|
|
2237
|
+
const results = [];
|
|
2238
|
+
for (const task of tasks) {
|
|
2239
|
+
const remaining = deadline ? Math.max(1, deadline - Date.now()) : void 0;
|
|
2240
|
+
results.push(await this.waitForTask(task.id, remaining));
|
|
2241
|
+
}
|
|
2242
|
+
return results;
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Cancel a task. If it's running, the task's AbortController fires.
|
|
2246
|
+
* Cascades to dependent tasks.
|
|
2247
|
+
*/
|
|
2248
|
+
async cancel(taskId, reason) {
|
|
2249
|
+
const { task, transitions } = await this.taskBoard.cancelTask(taskId, reason);
|
|
2250
|
+
await this.emitTransitions(transitions);
|
|
2251
|
+
if (task.memberId) {
|
|
2252
|
+
const runtime = this.members.get(task.memberId);
|
|
2253
|
+
if (runtime?.taskAbort && runtime.member.activeTaskId === taskId) {
|
|
2254
|
+
runtime.taskAbort.abort(reason ?? "Task cancelled");
|
|
2255
|
+
} else if (runtime && runtime.member.activeTaskId === taskId && !runtime.activeRun) {
|
|
2256
|
+
await this.finalizeExternallyTerminatedTask(runtime);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
if (this.taskDispatchMode === "manual") {
|
|
2260
|
+
await this.dispatchReady();
|
|
2261
|
+
}
|
|
2262
|
+
return task;
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Abort a running task. The member returns to idle and can pick up
|
|
2266
|
+
* new work. Unlike `cancel()`, abort preserves downstream tasks by
|
|
2267
|
+
* leaving them blocked on the aborted prerequisite.
|
|
2268
|
+
*/
|
|
2269
|
+
async abort(taskId, reason) {
|
|
2270
|
+
const task = await this.taskBoard.getTask(taskId);
|
|
2271
|
+
if (!task) throw new Error(`Unknown task: ${taskId}`);
|
|
2272
|
+
if (task.status !== "running" && task.status !== "claimed") {
|
|
2273
|
+
throw new Error(`Cannot abort task in "${task.status}" state`);
|
|
2274
|
+
}
|
|
2275
|
+
const { task: aborted, transitions } = await this.taskBoard.abortTask(taskId, reason);
|
|
2276
|
+
await this.emitTransitions(transitions);
|
|
2277
|
+
await this.emit({
|
|
2278
|
+
type: "team-task-aborted",
|
|
2279
|
+
teamId: this.teamId,
|
|
2280
|
+
taskId,
|
|
2281
|
+
memberId: aborted.memberId ?? "unassigned",
|
|
2282
|
+
reason
|
|
2283
|
+
});
|
|
2284
|
+
if (aborted.memberId) {
|
|
2285
|
+
const runtime = this.members.get(aborted.memberId);
|
|
2286
|
+
if (runtime?.taskAbort) {
|
|
2287
|
+
runtime.taskAbort.abort(reason ?? "Aborted by coordinator");
|
|
2288
|
+
} else if (runtime && this.externalTaskControl?.abortTask) {
|
|
2289
|
+
void this.externalTaskControl.abortTask({
|
|
2290
|
+
task: aborted,
|
|
2291
|
+
runtime,
|
|
2292
|
+
reason
|
|
2293
|
+
}).catch((err) => {
|
|
2294
|
+
this.log(`External abortTask failed for task ${taskId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2295
|
+
});
|
|
2296
|
+
} else if (runtime && runtime.member.activeTaskId === taskId && !runtime.activeRun) {
|
|
2297
|
+
await this.finalizeExternallyTerminatedTask(runtime);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
// ── Communication ──────────────────────────────────────────────────
|
|
2302
|
+
/**
|
|
2303
|
+
* Send a guidance message to a running member via intervention.
|
|
2304
|
+
* If the member is idle, queues a direct-message task instead.
|
|
2305
|
+
*/
|
|
2306
|
+
async guide(memberId, message) {
|
|
2307
|
+
const runtime = this.requireMember(memberId);
|
|
2308
|
+
const body = message.trim();
|
|
2309
|
+
if (!body) throw new Error("Message must not be empty");
|
|
2310
|
+
const record = await this.mailbox.send({
|
|
2311
|
+
teamId: this.teamId,
|
|
2312
|
+
from: "coordinator",
|
|
2313
|
+
to: memberId,
|
|
2314
|
+
kind: "direct",
|
|
2315
|
+
body,
|
|
2316
|
+
payload: { kind: "guidance", taskId: runtime.member.activeTaskId },
|
|
2317
|
+
taskId: runtime.member.activeTaskId,
|
|
2318
|
+
metadata: { payloadKind: "guidance" }
|
|
2319
|
+
});
|
|
2320
|
+
await this.emit({
|
|
2321
|
+
type: "team-message",
|
|
2322
|
+
teamId: this.teamId,
|
|
2323
|
+
message: record
|
|
2324
|
+
});
|
|
2325
|
+
if (runtime.member.status === "busy" && runtime.member.activeTaskId) {
|
|
2326
|
+
runtime.lastDeliveredMessageAt = record.createdAt;
|
|
2327
|
+
runtime.lastDeliveredMessageId = record.id;
|
|
2328
|
+
if (runtime.pendingAskResolve) {
|
|
2329
|
+
runtime.pendingAskResolve(body);
|
|
2330
|
+
this.log(`Ask response \u2192 @${memberId}: "${body.length > 80 ? body.slice(0, 80) + "\u2026" : body}"`);
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
if (runtime.taskAbort || this.taskDispatchMode !== "external") {
|
|
2334
|
+
runtime.agent.intervene(body);
|
|
2335
|
+
} else if (this.externalTaskControl?.guideTask) {
|
|
2336
|
+
const activeTask = await this.taskBoard.getTask(runtime.member.activeTaskId);
|
|
2337
|
+
if (activeTask) {
|
|
2338
|
+
void this.externalTaskControl.guideTask({
|
|
2339
|
+
task: activeTask,
|
|
2340
|
+
runtime,
|
|
2341
|
+
message: body
|
|
2342
|
+
}).catch((err) => {
|
|
2343
|
+
this.log(`External guideTask failed for @${memberId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
this.log(`Intervention \u2192 @${memberId}: "${body.length > 80 ? body.slice(0, 80) + "\u2026" : body}"`);
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
if (this.taskDispatchMode === "manual") {
|
|
2351
|
+
runtime.lastDeliveredMessageAt = record.createdAt;
|
|
2352
|
+
runtime.lastDeliveredMessageId = record.id;
|
|
2353
|
+
await this.queue({
|
|
2354
|
+
memberId,
|
|
2355
|
+
title: "guidance",
|
|
2356
|
+
prompt: body
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Broadcast a message to all members. Active members receive it
|
|
2362
|
+
* as an intervention; idle members see it on their next task.
|
|
2363
|
+
*/
|
|
2364
|
+
async broadcast(message) {
|
|
2365
|
+
const body = message.trim();
|
|
2366
|
+
if (!body) throw new Error("Message must not be empty");
|
|
2367
|
+
const record = await this.mailbox.send({
|
|
2368
|
+
teamId: this.teamId,
|
|
2369
|
+
from: "coordinator",
|
|
2370
|
+
kind: "broadcast",
|
|
2371
|
+
body,
|
|
2372
|
+
payload: { kind: "guidance" },
|
|
2373
|
+
metadata: { payloadKind: "guidance" }
|
|
2374
|
+
});
|
|
2375
|
+
await this.emit({
|
|
2376
|
+
type: "team-message",
|
|
2377
|
+
teamId: this.teamId,
|
|
2378
|
+
message: record
|
|
2379
|
+
});
|
|
2380
|
+
const recipients = [];
|
|
2381
|
+
for (const runtime of this.members.values()) {
|
|
2382
|
+
if (runtime.member.status === "busy") {
|
|
2383
|
+
runtime.lastDeliveredMessageAt = record.createdAt;
|
|
2384
|
+
runtime.lastDeliveredMessageId = record.id;
|
|
2385
|
+
if (runtime.taskAbort || this.taskDispatchMode !== "external") {
|
|
2386
|
+
runtime.agent.intervene(body);
|
|
2387
|
+
} else if (this.externalTaskControl?.guideTask && runtime.member.activeTaskId) {
|
|
2388
|
+
const activeTask = await this.taskBoard.getTask(runtime.member.activeTaskId);
|
|
2389
|
+
if (activeTask) {
|
|
2390
|
+
void this.externalTaskControl.guideTask({
|
|
2391
|
+
task: activeTask,
|
|
2392
|
+
runtime,
|
|
2393
|
+
message: body
|
|
2394
|
+
}).catch((err) => {
|
|
2395
|
+
this.log(`External guideTask (broadcast) failed for @${runtime.member.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
recipients.push(runtime.member.id);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
this.log(`Broadcast: "${body.length > 80 ? body.slice(0, 80) + "\u2026" : body}" \u2192 ${recipients.length > 0 ? recipients.join(", ") : "no active members"}`);
|
|
2403
|
+
return record;
|
|
2404
|
+
}
|
|
2405
|
+
// ── Shutdown protocol ──────────────────────────────────────────────
|
|
2406
|
+
/**
|
|
2407
|
+
* Request graceful shutdown of a specific member.
|
|
2408
|
+
*
|
|
2409
|
+
* - If member is idle, auto-accepts and goes offline immediately
|
|
2410
|
+
* - If member is busy, marks as shutting-down and waits for current
|
|
2411
|
+
* task to finish before going offline
|
|
2412
|
+
* - Returns true if shutdown succeeded within the timeout
|
|
2413
|
+
*/
|
|
2414
|
+
async requestShutdown(memberId, reason, timeoutMs) {
|
|
2415
|
+
return requestShutdown(this, memberId, reason, timeoutMs);
|
|
2416
|
+
}
|
|
2417
|
+
/**
|
|
2418
|
+
* Gracefully shut down all members.
|
|
2419
|
+
*/
|
|
2420
|
+
async shutdown(reason, timeoutMs) {
|
|
2421
|
+
return shutdownAll(this, reason, timeoutMs);
|
|
2422
|
+
}
|
|
2423
|
+
// ── Permission forwarding ──────────────────────────────────────────
|
|
2424
|
+
/**
|
|
2425
|
+
* Handle a permission request forwarded from a team member.
|
|
2426
|
+
* Called by the `teamPermissionPolicy` middleware when it has
|
|
2427
|
+
* a `forwardApproval` callback pointing at the coordinator.
|
|
2428
|
+
*/
|
|
2429
|
+
async handlePermissionRequest(memberId, tool, args) {
|
|
2430
|
+
const requestId = this.createId();
|
|
2431
|
+
await this.emit({
|
|
2432
|
+
type: "team-permission-forwarded",
|
|
2433
|
+
teamId: this.teamId,
|
|
2434
|
+
memberId,
|
|
2435
|
+
requestId,
|
|
2436
|
+
tool
|
|
2437
|
+
});
|
|
2438
|
+
let result;
|
|
2439
|
+
if (this.onPermissionRequest) {
|
|
2440
|
+
result = await this.onPermissionRequest(memberId, tool, args);
|
|
2441
|
+
} else {
|
|
2442
|
+
result = { decision: "allow" };
|
|
2443
|
+
}
|
|
2444
|
+
await this.emit({
|
|
2445
|
+
type: "team-permission-resolved",
|
|
2446
|
+
teamId: this.teamId,
|
|
2447
|
+
memberId,
|
|
2448
|
+
requestId,
|
|
2449
|
+
decision: result.decision
|
|
2450
|
+
});
|
|
2451
|
+
return result;
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Create a `forwardApproval` callback for `teamPermissionPolicy`
|
|
2455
|
+
* that routes through this coordinator's permission handler.
|
|
2456
|
+
*/
|
|
2457
|
+
createPermissionForwarder(memberId) {
|
|
2458
|
+
return async (_memberId, tool, args, _ctx) => {
|
|
2459
|
+
const result = await this.handlePermissionRequest(memberId, tool, args);
|
|
2460
|
+
return { action: result.decision, reason: result.reason };
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
/** @internal */
|
|
2464
|
+
beginCoordinatorTurnState() {
|
|
2465
|
+
this.coordinatorTurnState = {
|
|
2466
|
+
actions: {
|
|
2467
|
+
assigned: [],
|
|
2468
|
+
sentMessages: [],
|
|
2469
|
+
abortedTaskIds: []
|
|
2470
|
+
}
|
|
2471
|
+
};
|
|
2472
|
+
return this.coordinatorTurnState;
|
|
2473
|
+
}
|
|
2474
|
+
/** @internal */
|
|
2475
|
+
getActiveCoordinatorTurnState() {
|
|
2476
|
+
if (!this.coordinatorTurnState) {
|
|
2477
|
+
throw new Error("No active coordinator turn state");
|
|
2478
|
+
}
|
|
2479
|
+
return this.coordinatorTurnState;
|
|
2480
|
+
}
|
|
2481
|
+
/** @internal */
|
|
2482
|
+
endCoordinatorTurnState() {
|
|
2483
|
+
this.coordinatorTurnState = null;
|
|
2484
|
+
}
|
|
2485
|
+
/** @internal */
|
|
2486
|
+
getCoordinatorInboxSize() {
|
|
2487
|
+
return this.coordinatorInbox.length;
|
|
2488
|
+
}
|
|
2489
|
+
/** @internal */
|
|
2490
|
+
async getActiveTaskCount() {
|
|
2491
|
+
const tasks = await this.taskBoard.listTasks();
|
|
2492
|
+
return tasks.filter(
|
|
2493
|
+
(task) => task.status !== "completed" && task.status !== "aborted" && task.status !== "failed" && task.status !== "cancelled"
|
|
2494
|
+
).length;
|
|
2495
|
+
}
|
|
2496
|
+
/** @internal */
|
|
2497
|
+
ensureCoordinatorAgent() {
|
|
2498
|
+
if (!this.coordinatorAgent) {
|
|
2499
|
+
this.coordinatorAgent = createCoordinatorAgent(this);
|
|
2500
|
+
this.coordinatorSessionId = `team:${this.teamId}:coordinator`;
|
|
2501
|
+
this.attachCoordinatorTurn(this.coordinatorAgent, this.coordinatorSessionId);
|
|
2502
|
+
}
|
|
2503
|
+
return this.coordinatorAgent;
|
|
2504
|
+
}
|
|
2505
|
+
/** @internal */
|
|
2506
|
+
getCoordinatorSessionId() {
|
|
2507
|
+
return this.coordinatorSessionId;
|
|
2508
|
+
}
|
|
2509
|
+
resetCoordinatorRunSession() {
|
|
2510
|
+
const agent = this.ensureCoordinatorAgent();
|
|
2511
|
+
this.detachCoordinatorTurn();
|
|
2512
|
+
this.coordinatorInbox = [];
|
|
2513
|
+
this.coordinatorReschedule = false;
|
|
2514
|
+
this.coordinatorProcessing = false;
|
|
2515
|
+
this.endCoordinatorTurnState();
|
|
2516
|
+
this.coordinatorRoundNumber = 0;
|
|
2517
|
+
this.coordinatorRunSequence += 1;
|
|
2518
|
+
this.coordinatorSessionId = `team:${this.teamId}:coordinator:run-${this.coordinatorRunSequence}`;
|
|
2519
|
+
this.attachCoordinatorTurn(agent, this.coordinatorSessionId);
|
|
2520
|
+
this.publishCoordinatorStatus({
|
|
2521
|
+
phase: "ready",
|
|
2522
|
+
message: "Ready",
|
|
2523
|
+
pendingInboxCount: 0,
|
|
2524
|
+
activeTaskCount: 0
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
enqueueCoordinatorInboxItem(item) {
|
|
2528
|
+
enqueueInboxItem(this, item);
|
|
2529
|
+
}
|
|
2530
|
+
enqueueCoordinatorNotification(notification) {
|
|
2531
|
+
enqueueNotification(this, notification);
|
|
2532
|
+
}
|
|
2533
|
+
/** @internal */
|
|
2534
|
+
scheduleCoordinatorProcessing() {
|
|
2535
|
+
scheduleProcessing(this);
|
|
2536
|
+
}
|
|
2537
|
+
/** @internal */
|
|
2538
|
+
autonomousTurnCount = 0;
|
|
2539
|
+
async waitForCoordinatorIdle() {
|
|
2540
|
+
if (!this.coordinatorProcessing && this.coordinatorInbox.length === 0 && (this.coordinatorStatus.phase === "ready" || this.coordinatorStatus.phase === "error")) {
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
await new Promise((resolve) => {
|
|
2544
|
+
this.coordinatorIdleResolvers.push(resolve);
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
submitToCoordinator(message) {
|
|
2548
|
+
const body = message.trim();
|
|
2549
|
+
if (!body) {
|
|
2550
|
+
throw new Error("Coordinator message must not be empty");
|
|
2551
|
+
}
|
|
2552
|
+
const id = this.createId();
|
|
2553
|
+
this.log(`submitToCoordinator: id=${id} body="${body.length > 80 ? body.slice(0, 80) + "\u2026" : body}" (processing=${this.coordinatorProcessing}, inbox=${this.coordinatorInbox.length})`);
|
|
2554
|
+
this.enqueueCoordinatorInboxItem({
|
|
2555
|
+
id,
|
|
2556
|
+
kind: "user-message",
|
|
2557
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2558
|
+
body
|
|
2559
|
+
});
|
|
2560
|
+
this.scheduleCoordinatorProcessing();
|
|
2561
|
+
return id;
|
|
2562
|
+
}
|
|
2563
|
+
subscribeCoordinatorEvents(listener) {
|
|
2564
|
+
this.coordinatorEventListeners.add(listener);
|
|
2565
|
+
return () => {
|
|
2566
|
+
this.coordinatorEventListeners.delete(listener);
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
subscribeCoordinatorStatus(listener) {
|
|
2570
|
+
this.coordinatorStatusListeners.add(listener);
|
|
2571
|
+
listener({ ...this.coordinatorStatus });
|
|
2572
|
+
return () => {
|
|
2573
|
+
this.coordinatorStatusListeners.delete(listener);
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
getCoordinatorStatus() {
|
|
2577
|
+
return { ...this.coordinatorStatus };
|
|
2578
|
+
}
|
|
2579
|
+
/** @internal */
|
|
2580
|
+
attachCoordinatorTurn(agent, sessionId) {
|
|
2581
|
+
this.activeCoordinatorTurn = { agent, sessionId, turnOpen: false };
|
|
2582
|
+
}
|
|
2583
|
+
/** @internal */
|
|
2584
|
+
setCoordinatorTurnOpen(open) {
|
|
2585
|
+
if (!this.activeCoordinatorTurn) {
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
this.activeCoordinatorTurn.turnOpen = open;
|
|
2589
|
+
}
|
|
2590
|
+
/** @internal */
|
|
2591
|
+
detachCoordinatorTurn(agent) {
|
|
2592
|
+
if (!this.activeCoordinatorTurn) {
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
if (agent && this.activeCoordinatorTurn.agent !== agent) {
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
this.activeCoordinatorTurn = void 0;
|
|
2599
|
+
}
|
|
2600
|
+
// ── Coordinator loop ──────────────────────────────────────────────
|
|
2601
|
+
/**
|
|
2602
|
+
* Run the coordinator in an iterative loop.
|
|
2603
|
+
*
|
|
2604
|
+
* The lead agent is forked with delegation tools and a coordinator
|
|
2605
|
+
* system prompt. It reasons about the request, assigns tasks to
|
|
2606
|
+
* teammates, receives results, and continues until all work is done.
|
|
2607
|
+
*
|
|
2608
|
+
* This replaces the plan → fan-out → synthesize pipeline with a
|
|
2609
|
+
* dynamic multi-round conversation where the coordinator decides
|
|
2610
|
+
* what to delegate, how to react to results, and when it's done.
|
|
2611
|
+
*
|
|
2612
|
+
* Members must be registered before calling this method.
|
|
2613
|
+
*
|
|
2614
|
+
* @param prompt - The user's request
|
|
2615
|
+
* @param options - Loop configuration (max rounds, timeouts, progress callback)
|
|
2616
|
+
*/
|
|
2617
|
+
async run(prompt, options) {
|
|
2618
|
+
this.resetCoordinatorRunSession();
|
|
2619
|
+
const rounds = [];
|
|
2620
|
+
let totalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
2621
|
+
let lastResponse = "";
|
|
2622
|
+
const unsubEvents = this.subscribeCoordinatorEvents((event) => {
|
|
2623
|
+
options?.onCoordinatorEvent?.(event);
|
|
2624
|
+
if (event.type === "step-finish" && event.usage) {
|
|
2625
|
+
totalUsage = {
|
|
2626
|
+
inputTokens: (totalUsage.inputTokens ?? 0) + (event.usage.inputTokens ?? 0),
|
|
2627
|
+
outputTokens: (totalUsage.outputTokens ?? 0) + (event.usage.outputTokens ?? 0),
|
|
2628
|
+
totalTokens: (totalUsage.totalTokens ?? 0) + (event.usage.totalTokens ?? 0)
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
if (event.type === "complete" && typeof event.output === "string") {
|
|
2632
|
+
lastResponse = event.output;
|
|
2633
|
+
}
|
|
2634
|
+
});
|
|
2635
|
+
const unsubStatus = this.subscribeCoordinatorStatus((status) => {
|
|
2636
|
+
const mappedPhase = status.phase === "running" ? "thinking" : status.phase === "waiting" ? "waiting" : "done";
|
|
2637
|
+
options?.onStatus?.({
|
|
2638
|
+
phase: mappedPhase,
|
|
2639
|
+
round: this.coordinatorRoundNumber,
|
|
2640
|
+
message: status.message
|
|
2641
|
+
});
|
|
2642
|
+
});
|
|
2643
|
+
const unsubTeam = this.subscribeTeamEvents((event) => {
|
|
2644
|
+
if (event.type !== "team-coordinator-round") {
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
const round = {
|
|
2648
|
+
number: event.roundNumber,
|
|
2649
|
+
assigned: [...event.assigned],
|
|
2650
|
+
messaged: [...event.messaged],
|
|
2651
|
+
stopped: [...event.stopped],
|
|
2652
|
+
completions: [...event.completions],
|
|
2653
|
+
reports: [...event.reports],
|
|
2654
|
+
response: event.response,
|
|
2655
|
+
usage: event.usage
|
|
2656
|
+
};
|
|
2657
|
+
rounds.push(round);
|
|
2658
|
+
void options?.onRound?.(round);
|
|
2659
|
+
});
|
|
2660
|
+
try {
|
|
2661
|
+
this.submitToCoordinator(prompt);
|
|
2662
|
+
await this.waitForCoordinatorIdle();
|
|
2663
|
+
return { response: lastResponse, rounds, usage: totalUsage };
|
|
2664
|
+
} finally {
|
|
2665
|
+
unsubEvents();
|
|
2666
|
+
unsubStatus();
|
|
2667
|
+
unsubTeam();
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
// ── Planning ────────────────────────────────────────────────────────
|
|
2671
|
+
async plan(prompt, memberIds) {
|
|
2672
|
+
return planTasks(this, prompt, memberIds);
|
|
2673
|
+
}
|
|
2674
|
+
async planAndExecute(prompt, memberIds) {
|
|
2675
|
+
return planAndExecuteTasks(this, prompt, memberIds);
|
|
2676
|
+
}
|
|
2677
|
+
// ── Synthesis ──────────────────────────────────────────────────────
|
|
2678
|
+
/**
|
|
2679
|
+
* Use the lead agent to synthesize all completed task results
|
|
2680
|
+
* into a single coherent response.
|
|
2681
|
+
*
|
|
2682
|
+
* @param originalPrompt - The original task/prompt
|
|
2683
|
+
* @param taskIds - Optional subset of task IDs to synthesize (defaults to all terminal tasks)
|
|
2684
|
+
*/
|
|
2685
|
+
async synthesize(originalPrompt, taskIds) {
|
|
2686
|
+
return synthesizeResults(this, originalPrompt, taskIds);
|
|
2687
|
+
}
|
|
2688
|
+
// ── Queries ────────────────────────────────────────────────────────
|
|
2689
|
+
async snapshot() {
|
|
2690
|
+
const [board, messages] = await Promise.all([
|
|
2691
|
+
this.taskBoard.snapshot(),
|
|
2692
|
+
this.mailbox.list({ teamId: this.teamId })
|
|
2693
|
+
]);
|
|
2694
|
+
return { ...board, messages };
|
|
2695
|
+
}
|
|
2696
|
+
async listMembers() {
|
|
2697
|
+
return this.taskBoard.listMembers();
|
|
2698
|
+
}
|
|
2699
|
+
async listTasks(filter) {
|
|
2700
|
+
return this.taskBoard.listTasks(filter);
|
|
2701
|
+
}
|
|
2702
|
+
async listMessages(filter) {
|
|
2703
|
+
return this.mailbox.list({ ...filter, teamId: this.teamId });
|
|
2704
|
+
}
|
|
2705
|
+
getMemberSessionId(memberId) {
|
|
2706
|
+
return this.requireMember(memberId).member.sessionId;
|
|
2707
|
+
}
|
|
2708
|
+
getMemberRole(memberId) {
|
|
2709
|
+
return this.requireMember(memberId).role.name;
|
|
2710
|
+
}
|
|
2711
|
+
async prepareTaskForExternalExecution(taskId, runId) {
|
|
2712
|
+
const task = await this.taskBoard.getTask(taskId);
|
|
2713
|
+
if (!task?.memberId) {
|
|
2714
|
+
return void 0;
|
|
2715
|
+
}
|
|
2716
|
+
const runtime = this.members.get(task.memberId);
|
|
2717
|
+
if (!runtime) {
|
|
2718
|
+
return void 0;
|
|
2719
|
+
}
|
|
2720
|
+
return prepareTaskForExternalExecution(this, runtime, taskId, runId);
|
|
2721
|
+
}
|
|
2722
|
+
async completeExternalTask(taskId, result) {
|
|
2723
|
+
const task = await this.taskBoard.getTask(taskId);
|
|
2724
|
+
if (!task?.memberId) {
|
|
2725
|
+
return void 0;
|
|
2726
|
+
}
|
|
2727
|
+
const runtime = this.members.get(task.memberId);
|
|
2728
|
+
if (!runtime) {
|
|
2729
|
+
return void 0;
|
|
2730
|
+
}
|
|
2731
|
+
try {
|
|
2732
|
+
return await completePreparedTask(this, runtime, taskId, result);
|
|
2733
|
+
} finally {
|
|
2734
|
+
await this.finalizeExternallyTerminatedTask(runtime);
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
async failExternalTask(taskId, error) {
|
|
2738
|
+
const task = await this.taskBoard.getTask(taskId);
|
|
2739
|
+
if (!task?.memberId) {
|
|
2740
|
+
return void 0;
|
|
2741
|
+
}
|
|
2742
|
+
const runtime = this.members.get(task.memberId);
|
|
2743
|
+
if (!runtime) {
|
|
2744
|
+
return void 0;
|
|
2745
|
+
}
|
|
2746
|
+
try {
|
|
2747
|
+
return await failPreparedTask(this, runtime, taskId, error);
|
|
2748
|
+
} finally {
|
|
2749
|
+
await this.finalizeExternallyTerminatedTask(runtime);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
async abortExternalTask(taskId, reason) {
|
|
2753
|
+
const task = await this.taskBoard.getTask(taskId);
|
|
2754
|
+
if (!task?.memberId) {
|
|
2755
|
+
return void 0;
|
|
2756
|
+
}
|
|
2757
|
+
const runtime = this.members.get(task.memberId);
|
|
2758
|
+
if (!runtime) {
|
|
2759
|
+
return void 0;
|
|
2760
|
+
}
|
|
2761
|
+
try {
|
|
2762
|
+
return await abortPreparedTask(this, runtime, taskId, reason);
|
|
2763
|
+
} finally {
|
|
2764
|
+
await this.finalizeExternallyTerminatedTask(runtime);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
async getTask(taskId) {
|
|
2768
|
+
return this.taskBoard.getTask(taskId);
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Get aggregate stats for a member.
|
|
2772
|
+
* Returns undefined if the member is not registered.
|
|
2773
|
+
*/
|
|
2774
|
+
getMemberStats(memberId) {
|
|
2775
|
+
return this.members.get(memberId)?.stats;
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2778
|
+
* Subscribe to a member's agent events (text-delta, tool-start, etc.).
|
|
2779
|
+
* Returns an unsubscribe function. Throws if the member is not registered.
|
|
2780
|
+
*/
|
|
2781
|
+
onMemberEvent(memberId, handler) {
|
|
2782
|
+
const runtime = this.members.get(memberId);
|
|
2783
|
+
if (!runtime) {
|
|
2784
|
+
throw new Error(`Member "${memberId}" is not registered`);
|
|
2785
|
+
}
|
|
2786
|
+
return runtime.agent.signal.onAny(handler);
|
|
2787
|
+
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Serialize the coordinator state for persistence / resume.
|
|
2790
|
+
* Returns snapshot + member stats, enough to reconstruct state.
|
|
2791
|
+
*/
|
|
2792
|
+
async toJSON() {
|
|
2793
|
+
const snap = await this.snapshot();
|
|
2794
|
+
const memberStats = {};
|
|
2795
|
+
for (const [id, runtime] of this.members) {
|
|
2796
|
+
memberStats[id] = { ...runtime.stats };
|
|
2797
|
+
}
|
|
2798
|
+
return { teamId: this.teamId, snapshot: snap, memberStats };
|
|
2799
|
+
}
|
|
2800
|
+
// ── Internal: member state (CoordinatorCtx) ────────────────────────
|
|
2801
|
+
/** @internal */
|
|
2802
|
+
async updateMemberStatus(memberId, status, activeTaskId) {
|
|
2803
|
+
const runtime = this.members.get(memberId);
|
|
2804
|
+
if (!runtime) return;
|
|
2805
|
+
const previous = runtime.member.status;
|
|
2806
|
+
const updated = {
|
|
2807
|
+
...runtime.member,
|
|
2808
|
+
status,
|
|
2809
|
+
activeTaskId,
|
|
2810
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2811
|
+
};
|
|
2812
|
+
await this.taskBoard.putMember(updated);
|
|
2813
|
+
runtime.member = updated;
|
|
2814
|
+
if (previous !== status) {
|
|
2815
|
+
await this.emit({
|
|
2816
|
+
type: "team-member-status",
|
|
2817
|
+
teamId: this.teamId,
|
|
2818
|
+
memberId,
|
|
2819
|
+
previous,
|
|
2820
|
+
current: status,
|
|
2821
|
+
activeTaskId
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
requireMember(memberId) {
|
|
2826
|
+
const runtime = this.members.get(memberId);
|
|
2827
|
+
if (!runtime) {
|
|
2828
|
+
throw new Error(
|
|
2829
|
+
`Unknown member: "${memberId}". Registered: ${[...this.members.keys()].join(", ")}`
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
return runtime;
|
|
2833
|
+
}
|
|
2834
|
+
// ── Internal: events (CoordinatorCtx) ──────────────────────────────
|
|
2835
|
+
/** @internal */
|
|
2836
|
+
async emit(event) {
|
|
2837
|
+
await this.dispatchEvent(event);
|
|
2838
|
+
if (event.type === "team-notification") {
|
|
2839
|
+
this.log(`emit: team-notification kind=${event.notification.kind} member=${event.notification.memberId}`);
|
|
2840
|
+
this.enqueueCoordinatorNotification(event.notification);
|
|
2841
|
+
this.scheduleCoordinatorProcessing();
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
try {
|
|
2845
|
+
const notificationEvent = await buildCoordinatorNotificationEvent(
|
|
2846
|
+
event,
|
|
2847
|
+
(taskId) => this.taskBoard.getTask(taskId)
|
|
2848
|
+
);
|
|
2849
|
+
if (notificationEvent) {
|
|
2850
|
+
this.log(`emit: ${event.type} \u2192 notification kind=${notificationEvent.notification.kind} member=${notificationEvent.notification.memberId}`);
|
|
2851
|
+
await this.dispatchEvent(notificationEvent);
|
|
2852
|
+
this.enqueueCoordinatorNotification(notificationEvent.notification);
|
|
2853
|
+
this.scheduleCoordinatorProcessing();
|
|
2854
|
+
}
|
|
2855
|
+
} catch {
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
/** @internal */
|
|
2859
|
+
async emitTransitions(transitions) {
|
|
2860
|
+
for (const t of transitions) {
|
|
2861
|
+
if (t.reason === "created") {
|
|
2862
|
+
await this.emit({
|
|
2863
|
+
type: "team-task-created",
|
|
2864
|
+
teamId: this.teamId,
|
|
2865
|
+
task: t.task
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
if (t.previous !== void 0) {
|
|
2869
|
+
this.log(`Task "${t.task.title}" (${t.task.memberId ?? "?"}): ${t.previous} \u2192 ${t.task.status}${t.reason ? ` (${t.reason})` : ""}`);
|
|
2870
|
+
await this.emit({
|
|
2871
|
+
type: "team-task-transition",
|
|
2872
|
+
teamId: this.teamId,
|
|
2873
|
+
taskId: t.task.id,
|
|
2874
|
+
previous: t.previous,
|
|
2875
|
+
current: t.task.status,
|
|
2876
|
+
reason: t.reason
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
async finalizeExternallyTerminatedTask(runtime) {
|
|
2882
|
+
if (runtime.stopRequested || runtime.member.status === "shutting-down") {
|
|
2883
|
+
await this.updateMemberStatus(runtime.member.id, "offline", void 0);
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
await this.updateMemberStatus(runtime.member.id, "idle", void 0);
|
|
2887
|
+
if (this.taskDispatchMode === "manual") {
|
|
2888
|
+
await this.dispatchReady();
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
};
|
|
2892
|
+
function createTeamCoordinator(config) {
|
|
2893
|
+
return new TeamCoordinator(config);
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
// src/team/coordinator/round-engine.ts
|
|
2897
|
+
function addTokenUsage(total, usage) {
|
|
2898
|
+
return {
|
|
2899
|
+
inputTokens: (total.inputTokens ?? 0) + (usage?.inputTokens ?? 0),
|
|
2900
|
+
outputTokens: (total.outputTokens ?? 0) + (usage?.outputTokens ?? 0),
|
|
2901
|
+
totalTokens: (total.totalTokens ?? 0) + (usage?.totalTokens ?? 0)
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
function evaluateCoordinatorRoundTransition(input) {
|
|
2905
|
+
if (input.reasonAssignmentsCount === 0 && input.activeTaskCount === 0 && input.terminalAfterEvent && input.reports.length === 0) {
|
|
2906
|
+
return {
|
|
2907
|
+
kind: "done",
|
|
2908
|
+
finalResponse: input.reasonResponse
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
if (input.notifications.length === 0 && input.reports.length === 0 && input.activeTaskCount === 0) {
|
|
2912
|
+
if (!input.terminalAfterEvent) {
|
|
2913
|
+
return {
|
|
2914
|
+
kind: "continue",
|
|
2915
|
+
nextMessage: input.blockedMessage
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
return {
|
|
2919
|
+
kind: "done",
|
|
2920
|
+
finalResponse: input.reasonAssignmentsCount === 0 && input.terminalAfterEvent ? input.reasonResponse : input.roundResponse
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
if (input.notifications.length === 0 && input.reports.length === 0 && input.activeTaskCount > 0) {
|
|
2924
|
+
return {
|
|
2925
|
+
kind: "wait",
|
|
2926
|
+
nextMessage: ""
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
return {
|
|
2930
|
+
kind: "continue",
|
|
2931
|
+
nextMessage: formatCoordinatorRoundMessage(
|
|
2932
|
+
input.notifications,
|
|
2933
|
+
input.reports
|
|
2934
|
+
)
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
export {
|
|
2939
|
+
TERMINAL_STATUSES,
|
|
2940
|
+
TaskConflictError,
|
|
2941
|
+
InMemoryTaskBoardStore,
|
|
2942
|
+
TaskBoard,
|
|
2943
|
+
InMemoryMailboxStore,
|
|
2944
|
+
Mailbox,
|
|
2945
|
+
teamPermissionPolicy,
|
|
2946
|
+
coordinatorToolDescriptions,
|
|
2947
|
+
buildCoordinatorSystemPrompt,
|
|
2948
|
+
formatCoordinatorTaskNotifications,
|
|
2949
|
+
formatCoordinatorWorkerReports,
|
|
2950
|
+
formatCoordinatorRoundMessage,
|
|
2951
|
+
buildCoordinatorNotificationEvent,
|
|
2952
|
+
TeamCoordinator,
|
|
2953
|
+
createTeamCoordinator,
|
|
2954
|
+
addTokenUsage,
|
|
2955
|
+
evaluateCoordinatorRoundTransition
|
|
2956
|
+
};
|