@qilin-agent/experimental-agent-team 0.0.0-stage → 3.1.4
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/LICENSE +21 -0
- package/README.i18n.yaml +103 -0
- package/README.md +225 -2
- package/README.zh.md +226 -0
- package/lib/index.js +1782 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +875 -0
- package/lib/typert.remote-client.d.ts +30 -0
- package/lib/typert.remote-client.js +141 -0
- package/lib/types/activity.d.ts +23 -0
- package/lib/types/activity.js +85 -0
- package/lib/types/client.d.ts +8 -0
- package/lib/types/client.js +3 -0
- package/lib/types/error.d.ts +13 -0
- package/lib/types/error.js +23 -0
- package/lib/types/index.d.ts +148 -0
- package/lib/types/index.js +340 -0
- package/lib/types/journal.d.ts +39 -0
- package/lib/types/journal.js +63 -0
- package/lib/types/lifecycle.d.ts +45 -0
- package/lib/types/lifecycle.js +103 -0
- package/lib/types/persisted.d.ts +20 -0
- package/lib/types/persisted.js +20 -0
- package/lib/types/projection.d.ts +66 -0
- package/lib/types/projection.js +327 -0
- package/lib/types/remote.d.ts +31 -0
- package/lib/types/remote.js +22 -0
- package/lib/types/roster.d.ts +107 -0
- package/lib/types/roster.js +441 -0
- package/lib/types/session-message.d.ts +11 -0
- package/lib/types/session-message.js +23 -0
- package/lib/types/task-board.d.ts +53 -0
- package/lib/types/task-board.js +235 -0
- package/lib/types/task-graph.d.ts +21 -0
- package/lib/types/task-graph.js +62 -0
- package/lib/types/task-view.d.ts +21 -0
- package/lib/types/task-view.js +57 -0
- package/lib/types/types.d.ts +212 -0
- package/lib/types/types.js +26 -0
- package/lib/types/validation.d.ts +16 -0
- package/lib/types/validation.js +33 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +83 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,1782 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import z from "@qilin-agent/schemastery";
|
|
3
|
+
import { HarnessError, createUserMessage } from "@qilin-agent/llm";
|
|
4
|
+
import { steerHostSubagentPrompt } from "@qilin-agent/subagent/internal";
|
|
5
|
+
import { Remote, RemoteError, TypertRemoteService } from "@qilin-agent/typert-protocol";
|
|
6
|
+
import { inspect } from "node:util";
|
|
7
|
+
import { z as z$1 } from "zod";
|
|
8
|
+
import { brandString } from "@qilin-agent/brand";
|
|
9
|
+
import { foldSubagentDescriptor } from "@qilin-agent/subagent";
|
|
10
|
+
//#region lib/types/error.js
|
|
11
|
+
/** Typed Agent Teams failures. */
|
|
12
|
+
/** Stable failure raised by the Team domain. */
|
|
13
|
+
var TeamError = class extends HarnessError {
|
|
14
|
+
constructor(message, code, options) {
|
|
15
|
+
super(message, code, options);
|
|
16
|
+
this.name = "TeamError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Render an arbitrary thrown value without replacing the original rejection.
|
|
21
|
+
* @param error - caught value used in a diagnostic or durable failure record.
|
|
22
|
+
* @returns one bounded single-line description.
|
|
23
|
+
*/
|
|
24
|
+
function errorMessage(error) {
|
|
25
|
+
if (error instanceof Error) return error.message;
|
|
26
|
+
if (typeof error === "string") return error;
|
|
27
|
+
return inspect(error, {
|
|
28
|
+
breakLength: Infinity,
|
|
29
|
+
compact: true,
|
|
30
|
+
depth: 4
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region lib/types/activity.js
|
|
35
|
+
/** One-shot Team change waiters independent of durable state projection. */
|
|
36
|
+
/** Owns current Team change waiters and releases each at most once. */
|
|
37
|
+
var TeamActivity = class {
|
|
38
|
+
waiters = /* @__PURE__ */ new Map();
|
|
39
|
+
closed = false;
|
|
40
|
+
/**
|
|
41
|
+
* Wait for one later Team-domain or member-status change.
|
|
42
|
+
* @param id - Team whose next edge wakes the caller.
|
|
43
|
+
* @param timeoutMs - bounded wait duration from ten seconds through one hour.
|
|
44
|
+
* @param signal - caller cancellation for this wait only.
|
|
45
|
+
* @returns whether the wait ended by timeout.
|
|
46
|
+
*/
|
|
47
|
+
async wait(id, timeoutMs, signal) {
|
|
48
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1e4 || timeoutMs > 36e5) throw new TeamError("timeoutMs must be an integer from 10000 through 3600000", "TEAM_INVALID_TIMEOUT");
|
|
49
|
+
signal.throwIfAborted();
|
|
50
|
+
if (this.closed) return { timedOut: false };
|
|
51
|
+
return { timedOut: !await new Promise((resolve, reject) => {
|
|
52
|
+
let waiters = this.waiters.get(id);
|
|
53
|
+
if (waiters === void 0) {
|
|
54
|
+
waiters = /* @__PURE__ */ new Set();
|
|
55
|
+
this.waiters.set(id, waiters);
|
|
56
|
+
}
|
|
57
|
+
let settled = false;
|
|
58
|
+
const finish = (settle) => {
|
|
59
|
+
/* v8 ignore next -- timeout, abort, and notification may race after one winner removes the others. */
|
|
60
|
+
if (settled) return;
|
|
61
|
+
settled = true;
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
signal.removeEventListener("abort", onAbort);
|
|
64
|
+
waiters.delete(waiter);
|
|
65
|
+
if (waiters.size === 0) this.waiters.delete(id);
|
|
66
|
+
settle();
|
|
67
|
+
};
|
|
68
|
+
const onAbort = () => {
|
|
69
|
+
finish(() => {
|
|
70
|
+
const reason = signal.reason;
|
|
71
|
+
reject(reason instanceof Error ? reason : new TeamError(`wait_agent aborted: ${errorMessage(reason)}`, "TEAM_WAIT_ABORTED"));
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const waiter = { resolve: () => {
|
|
75
|
+
finish(() => {
|
|
76
|
+
resolve(true);
|
|
77
|
+
});
|
|
78
|
+
} };
|
|
79
|
+
waiters.add(waiter);
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
finish(() => {
|
|
82
|
+
resolve(false);
|
|
83
|
+
});
|
|
84
|
+
}, timeoutMs);
|
|
85
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
86
|
+
/* v8 ignore next -- requires an abort in the synchronous gap between the pre-check and listener registration. */
|
|
87
|
+
if (signal.aborted) onAbort();
|
|
88
|
+
}) };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Wake and remove every current waiter for one Team.
|
|
92
|
+
* @param id - Team whose current waiters observe the change.
|
|
93
|
+
*/
|
|
94
|
+
notify(id) {
|
|
95
|
+
const waiters = this.waiters.get(id);
|
|
96
|
+
if (waiters === void 0) return;
|
|
97
|
+
this.waiters.delete(id);
|
|
98
|
+
for (const waiter of waiters) waiter.resolve();
|
|
99
|
+
}
|
|
100
|
+
/** Close admission and wake every current waiter during runtime disposal. */
|
|
101
|
+
close() {
|
|
102
|
+
this.closed = true;
|
|
103
|
+
for (const waiters of this.waiters.values()) for (const waiter of waiters) waiter.resolve();
|
|
104
|
+
this.waiters.clear();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region lib/types/journal.js
|
|
109
|
+
/** Serialized Team transactions over the exact live Lead Session log. */
|
|
110
|
+
/** Owns per-Lead transaction order and committed Team event publication. */
|
|
111
|
+
var TeamJournal = class {
|
|
112
|
+
ctx;
|
|
113
|
+
onCommit;
|
|
114
|
+
tails = /* @__PURE__ */ new Map();
|
|
115
|
+
/**
|
|
116
|
+
* @param ctx - Team service context with the injected Session service.
|
|
117
|
+
* @param onCommit - synchronous notification after the Team event flush succeeds.
|
|
118
|
+
*/
|
|
119
|
+
constructor(ctx, onCommit) {
|
|
120
|
+
this.ctx = ctx;
|
|
121
|
+
this.onCommit = onCommit;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Read authoritative Team state for one exact live Lead.
|
|
125
|
+
* @param root - exact live Team Lead.
|
|
126
|
+
* @returns current projected state selected by the Lead Team id.
|
|
127
|
+
*/
|
|
128
|
+
state(root) {
|
|
129
|
+
const projection = this.ctx.sessionProjections.stateOf(root.session, "agentTeam");
|
|
130
|
+
if (projection === void 0) throw new Error("Agent Teams projection is not registered");
|
|
131
|
+
if (projection.failure !== void 0) throw new Error(projection.failure);
|
|
132
|
+
return projection;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Serialize one Lead's asynchronous mutation operation.
|
|
136
|
+
* @param rootId - Lead Session identity selecting the transaction queue.
|
|
137
|
+
* @param operation - complete read-check-append operation.
|
|
138
|
+
* @returns the operation result.
|
|
139
|
+
*/
|
|
140
|
+
async transact(rootId, operation) {
|
|
141
|
+
const run = (this.tails.get(rootId) ?? Promise.resolve()).then(operation, operation);
|
|
142
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
143
|
+
this.tails.set(rootId, tail);
|
|
144
|
+
try {
|
|
145
|
+
return await run;
|
|
146
|
+
} finally {
|
|
147
|
+
if (this.tails.get(rootId) === tail) this.tails.delete(rootId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Append and checkpoint one root-owned Team event before publication.
|
|
152
|
+
* @param root - exact live Lead whose Session owns the event.
|
|
153
|
+
* @param type - Team event discriminant.
|
|
154
|
+
* @param data - payload correlated with the event type.
|
|
155
|
+
*/
|
|
156
|
+
async appendAndFlush(root, type, data) {
|
|
157
|
+
root.session.append.bind(root.session)(type, data);
|
|
158
|
+
await this.ctx.sessions.flush(root.session);
|
|
159
|
+
this.onCommit(root);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region lib/types/lifecycle.js
|
|
164
|
+
/** Shared admission cutoff and bounded settlement for the Team runtime. */
|
|
165
|
+
/** Owns the single Team runtime cancellation fact and disposal timeout. */
|
|
166
|
+
var TeamRuntimeLifecycle = class {
|
|
167
|
+
disposalTimeoutMs;
|
|
168
|
+
operations = /* @__PURE__ */ new Set();
|
|
169
|
+
controller = new AbortController();
|
|
170
|
+
/**
|
|
171
|
+
* @param disposalTimeoutMs - maximum wait for one disposal settlement operation.
|
|
172
|
+
*/
|
|
173
|
+
constructor(disposalTimeoutMs) {
|
|
174
|
+
this.disposalTimeoutMs = disposalTimeoutMs;
|
|
175
|
+
}
|
|
176
|
+
/** Signal aborted exactly when Team runtime admission closes. */
|
|
177
|
+
get signal() {
|
|
178
|
+
return this.controller.signal;
|
|
179
|
+
}
|
|
180
|
+
/** Whether Team runtime admission is closed. */
|
|
181
|
+
get disposed() {
|
|
182
|
+
return this.signal.aborted;
|
|
183
|
+
}
|
|
184
|
+
/** The exact cancellation reason used to distinguish expected disposal rejection. */
|
|
185
|
+
get reason() {
|
|
186
|
+
return this.signal.reason;
|
|
187
|
+
}
|
|
188
|
+
/** Whether a rejection is the runtime cancellation, directly or through an Error cause chain. */
|
|
189
|
+
isCancellation(reason) {
|
|
190
|
+
const seen = /* @__PURE__ */ new Set();
|
|
191
|
+
let current = reason;
|
|
192
|
+
while (!seen.has(current)) {
|
|
193
|
+
if (this.disposed && current === this.reason) return true;
|
|
194
|
+
if (this.disposed && current instanceof TeamError && current.code === "TEAM_DISPOSED") return true;
|
|
195
|
+
if (!(current instanceof Error)) return false;
|
|
196
|
+
seen.add(current);
|
|
197
|
+
current = current.cause;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
/** Close Team runtime admission and cancel admitted interruptible work. */
|
|
202
|
+
close() {
|
|
203
|
+
this.controller.abort(new TeamError("Agent Teams service disposed", "TEAM_DISPOSED"));
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Track an admitted send or complete creation transaction until it settles.
|
|
207
|
+
* @param operation - asynchronous work owned by this runtime.
|
|
208
|
+
* @returns the same operation.
|
|
209
|
+
*/
|
|
210
|
+
track(operation) {
|
|
211
|
+
this.operations.add(operation);
|
|
212
|
+
operation.then(() => {
|
|
213
|
+
this.operations.delete(operation);
|
|
214
|
+
}, () => {
|
|
215
|
+
this.operations.delete(operation);
|
|
216
|
+
});
|
|
217
|
+
return operation;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Capture outstanding operations after closing admission.
|
|
221
|
+
* @returns admitted work still awaiting settlement.
|
|
222
|
+
*/
|
|
223
|
+
pending() {
|
|
224
|
+
return [...this.operations];
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Await admitted operations and retain failures other than runtime cancellation.
|
|
228
|
+
* @param operations - admitted operations captured after the admission cutoff.
|
|
229
|
+
* @param failures - aggregate destination for unexpected rejection or timeout.
|
|
230
|
+
*/
|
|
231
|
+
async settle(operations, failures) {
|
|
232
|
+
if (operations.length === 0) return;
|
|
233
|
+
try {
|
|
234
|
+
const outcomes = await this.withTimeout(Promise.allSettled(operations));
|
|
235
|
+
for (const outcome of outcomes) if (outcome.status === "rejected" && !this.isCancellation(outcome.reason)) failures.push(outcome.reason);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
failures.push(error);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Bound one runtime settlement operation.
|
|
242
|
+
* @param operation - settlement that may otherwise block HMR or process shutdown.
|
|
243
|
+
* @returns the operation result.
|
|
244
|
+
*/
|
|
245
|
+
async withTimeout(operation) {
|
|
246
|
+
let timer;
|
|
247
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
248
|
+
timer = setTimeout(() => {
|
|
249
|
+
reject(new TeamError(`Agent Teams runtime disposal exceeded ${this.disposalTimeoutMs}ms`, "TEAM_DISPOSAL_TIMEOUT"));
|
|
250
|
+
}, this.disposalTimeoutMs);
|
|
251
|
+
});
|
|
252
|
+
try {
|
|
253
|
+
return await Promise.race([operation, timeout]);
|
|
254
|
+
} finally {
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region lib/types/types.js
|
|
261
|
+
/** Public Agent Teams identities, durable records, and service request values. */
|
|
262
|
+
/**
|
|
263
|
+
* Brand one root Session identity as its implicit Team identity.
|
|
264
|
+
* @param id - Root Session identity.
|
|
265
|
+
* @returns the same string branded as a Team identity.
|
|
266
|
+
*/
|
|
267
|
+
function TeamId(id) {
|
|
268
|
+
return id;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Brand a validated task id.
|
|
272
|
+
* @param id - Team-local task identity.
|
|
273
|
+
* @returns the same string branded as a Team task identity.
|
|
274
|
+
*/
|
|
275
|
+
function TeamTaskId(id) {
|
|
276
|
+
return id;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Brand a generated peer-message id.
|
|
280
|
+
* @param id - Durable mailbox message identity.
|
|
281
|
+
* @returns the same string branded as a Team message identity.
|
|
282
|
+
*/
|
|
283
|
+
function TeamMessageId(id) {
|
|
284
|
+
return id;
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region lib/types/task-graph.js
|
|
288
|
+
/** Complete dependency validation for current Team task snapshots. */
|
|
289
|
+
/** Package-private task dependency failure retained for command error mapping. */
|
|
290
|
+
var TeamTaskGraphError = class extends Error {
|
|
291
|
+
violation;
|
|
292
|
+
/**
|
|
293
|
+
* @param message - concrete invalid dependency relation.
|
|
294
|
+
* @param violation - stable relation category used by Team commands.
|
|
295
|
+
*/
|
|
296
|
+
constructor(message, violation) {
|
|
297
|
+
super(message);
|
|
298
|
+
this.violation = violation;
|
|
299
|
+
this.name = "TeamTaskGraphError";
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Validate the complete active task graph after replacing one candidate snapshot.
|
|
304
|
+
* @param current - current task snapshots before the candidate event.
|
|
305
|
+
* @param candidate - new or next-revision task snapshot.
|
|
306
|
+
* @throws {TeamTaskGraphError} when an active dependency is missing, duplicated, self-referential, or cyclic.
|
|
307
|
+
*/
|
|
308
|
+
function assertTaskGraphCandidate(current, candidate) {
|
|
309
|
+
const tasks = new Map(current.map((task) => [task.id, task]));
|
|
310
|
+
tasks.set(candidate.id, candidate);
|
|
311
|
+
for (const task of tasks.values()) {
|
|
312
|
+
if (task.status === "deleted") continue;
|
|
313
|
+
const seen = /* @__PURE__ */ new Set();
|
|
314
|
+
for (const blockerId of task.blockedBy) {
|
|
315
|
+
if (blockerId === task.id) throw new TeamTaskGraphError(`team task "${task.id}" cannot block itself`, "cycle");
|
|
316
|
+
if (seen.has(blockerId)) throw new TeamTaskGraphError(`team task "${task.id}" repeats blocker "${blockerId}"`, "duplicate");
|
|
317
|
+
const blocker = tasks.get(blockerId);
|
|
318
|
+
if (blocker === void 0 || blocker.status === "deleted") throw new TeamTaskGraphError(`blocker task "${blockerId}" for "${task.id}" is missing or deleted`, "missing");
|
|
319
|
+
seen.add(blockerId);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
323
|
+
const visited = /* @__PURE__ */ new Set();
|
|
324
|
+
const visit = (id) => {
|
|
325
|
+
if (visiting.has(id)) throw new TeamTaskGraphError(`task dependency cycle includes "${id}"`, "cycle");
|
|
326
|
+
if (visited.has(id)) return;
|
|
327
|
+
const task = tasks.get(id);
|
|
328
|
+
if (task === void 0 || task.status === "deleted") return;
|
|
329
|
+
visiting.add(id);
|
|
330
|
+
for (const blockerId of task.blockedBy) visit(blockerId);
|
|
331
|
+
visiting.delete(id);
|
|
332
|
+
visited.add(id);
|
|
333
|
+
};
|
|
334
|
+
for (const task of tasks.values()) visit(task.id);
|
|
335
|
+
}
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region lib/types/task-view.js
|
|
338
|
+
/** Pure task-view derivation shared by the task board and the client projection. */
|
|
339
|
+
/**
|
|
340
|
+
* Whether two normalized file or directory prefixes overlap on path components.
|
|
341
|
+
* @param left - normalized write scope.
|
|
342
|
+
* @param right - normalized write scope.
|
|
343
|
+
* @returns whether either scope contains the other.
|
|
344
|
+
*/
|
|
345
|
+
function scopesOverlap(left, right) {
|
|
346
|
+
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Whether every current blocker of one task completed.
|
|
350
|
+
* @param state - Team state supplying sibling tasks.
|
|
351
|
+
* @param task - durable task snapshot to test.
|
|
352
|
+
* @returns whether the task has no incomplete blockers.
|
|
353
|
+
*/
|
|
354
|
+
function taskReady(state, task) {
|
|
355
|
+
return task.blockedBy.every((id) => state.tasks.find((candidate) => candidate.id === id)?.status === "completed");
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Derive one task view with owner name, readiness, and advisory write overlaps.
|
|
359
|
+
* A committing caller may pass its pre-append state because `task` supplies the
|
|
360
|
+
* new value explicitly; owner names, blocker readiness, and other task scopes
|
|
361
|
+
* do not change when that snapshot is appended.
|
|
362
|
+
* @param state - Team state supplying members and sibling tasks.
|
|
363
|
+
* @param task - durable task snapshot to view.
|
|
364
|
+
* @returns a detached task view.
|
|
365
|
+
*/
|
|
366
|
+
function projectTaskView(state, task) {
|
|
367
|
+
const ownerName = task.ownerId === void 0 ? void 0 : task.ownerId === brandString(state.id) ? "lead" : state.members.find((member) => member.id === task.ownerId)?.name;
|
|
368
|
+
const warnings = /* @__PURE__ */ new Set();
|
|
369
|
+
for (const other of state.tasks) {
|
|
370
|
+
if (other.id === task.id || other.status !== "in_progress") continue;
|
|
371
|
+
if (task.writeScopes.some((left) => other.writeScopes.some((right) => scopesOverlap(left, right)))) warnings.add(`write scopes overlap with ${other.id}`);
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
id: task.id,
|
|
375
|
+
revision: task.revision,
|
|
376
|
+
subject: task.subject,
|
|
377
|
+
description: task.description,
|
|
378
|
+
status: task.status,
|
|
379
|
+
blockedBy: [...task.blockedBy],
|
|
380
|
+
writeScopes: [...task.writeScopes],
|
|
381
|
+
...ownerName === void 0 ? {} : { ownerName },
|
|
382
|
+
ready: task.status === "pending" && taskReady(state, task),
|
|
383
|
+
writeScopeWarnings: [...warnings]
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region lib/types/projection.js
|
|
388
|
+
/** Team state projected incrementally from committed Session events, with a durable-only client view. */
|
|
389
|
+
const nonNegativeSafeInteger = z$1.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
390
|
+
const positiveSafeInteger = nonNegativeSafeInteger.min(1);
|
|
391
|
+
const sessionIdSchema = z$1.string().min(1).transform((value) => brandString(value));
|
|
392
|
+
const teamIdSchema = z$1.string().min(1).transform((value) => TeamId(value));
|
|
393
|
+
const numericTaskIdPattern = /^task-(\d+)$/u;
|
|
394
|
+
const teamTaskIdSchema = z$1.string().min(1).refine((value) => {
|
|
395
|
+
const match = numericTaskIdPattern.exec(value);
|
|
396
|
+
return match === null || Number.isSafeInteger(Number(match[1]));
|
|
397
|
+
}, { message: "numeric task id suffix must be a safe integer" }).transform((value) => TeamTaskId(value));
|
|
398
|
+
const teamMessageIdSchema = z$1.string().min(1).transform((value) => TeamMessageId(value));
|
|
399
|
+
const coreContentBlockTypes = new Set([
|
|
400
|
+
"text",
|
|
401
|
+
"reasoning",
|
|
402
|
+
"image",
|
|
403
|
+
"tool-call",
|
|
404
|
+
"tool-result"
|
|
405
|
+
]);
|
|
406
|
+
const imageAttachmentSchema = z$1.object({
|
|
407
|
+
attachmentId: z$1.string().min(1),
|
|
408
|
+
mediaType: z$1.enum([
|
|
409
|
+
"image/png",
|
|
410
|
+
"image/jpeg",
|
|
411
|
+
"image/webp",
|
|
412
|
+
"image/gif"
|
|
413
|
+
]),
|
|
414
|
+
bytes: nonNegativeSafeInteger,
|
|
415
|
+
width: positiveSafeInteger,
|
|
416
|
+
height: positiveSafeInteger,
|
|
417
|
+
name: z$1.string().optional()
|
|
418
|
+
}).strict();
|
|
419
|
+
const contentBlockSchema = z$1.lazy(() => z$1.union([
|
|
420
|
+
z$1.object({
|
|
421
|
+
type: z$1.literal("text"),
|
|
422
|
+
text: z$1.string()
|
|
423
|
+
}).strict(),
|
|
424
|
+
z$1.object({
|
|
425
|
+
type: z$1.literal("reasoning"),
|
|
426
|
+
text: z$1.string()
|
|
427
|
+
}).strict(),
|
|
428
|
+
z$1.object({
|
|
429
|
+
type: z$1.literal("image"),
|
|
430
|
+
attachment: imageAttachmentSchema
|
|
431
|
+
}).strict(),
|
|
432
|
+
z$1.object({
|
|
433
|
+
type: z$1.literal("tool-call"),
|
|
434
|
+
id: z$1.string().min(1),
|
|
435
|
+
name: z$1.string(),
|
|
436
|
+
arguments: z$1.string()
|
|
437
|
+
}).strict(),
|
|
438
|
+
z$1.custom((value) => {
|
|
439
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
440
|
+
const type = value.type;
|
|
441
|
+
return typeof type === "string" && type.length > 0 && !coreContentBlockTypes.has(type);
|
|
442
|
+
})
|
|
443
|
+
]));
|
|
444
|
+
const teamMemberSnapshotSchema = z$1.object({
|
|
445
|
+
id: sessionIdSchema,
|
|
446
|
+
name: z$1.string(),
|
|
447
|
+
description: z$1.string(),
|
|
448
|
+
provider: z$1.string(),
|
|
449
|
+
context: z$1.enum(["fresh", "fork"]),
|
|
450
|
+
phase: z$1.enum([
|
|
451
|
+
"provisioning",
|
|
452
|
+
"active",
|
|
453
|
+
"failed"
|
|
454
|
+
]),
|
|
455
|
+
error: z$1.string().optional()
|
|
456
|
+
}).strict();
|
|
457
|
+
const teamTaskSnapshotSchema = z$1.object({
|
|
458
|
+
id: teamTaskIdSchema,
|
|
459
|
+
revision: positiveSafeInteger,
|
|
460
|
+
subject: z$1.string(),
|
|
461
|
+
description: z$1.string(),
|
|
462
|
+
status: z$1.enum([
|
|
463
|
+
"pending",
|
|
464
|
+
"in_progress",
|
|
465
|
+
"completed",
|
|
466
|
+
"deleted"
|
|
467
|
+
]),
|
|
468
|
+
ownerId: sessionIdSchema.optional(),
|
|
469
|
+
blockedBy: z$1.array(teamTaskIdSchema),
|
|
470
|
+
writeScopes: z$1.array(z$1.string())
|
|
471
|
+
}).strict();
|
|
472
|
+
const teamMessageSnapshotSchema = z$1.object({
|
|
473
|
+
id: teamMessageIdSchema,
|
|
474
|
+
senderId: sessionIdSchema,
|
|
475
|
+
senderName: z$1.string(),
|
|
476
|
+
targetId: sessionIdSchema,
|
|
477
|
+
content: z$1.array(contentBlockSchema)
|
|
478
|
+
}).strict();
|
|
479
|
+
const teamEventSelectorSchema = z$1.object({
|
|
480
|
+
version: nonNegativeSafeInteger,
|
|
481
|
+
teamId: teamIdSchema
|
|
482
|
+
}).loose();
|
|
483
|
+
const teamMemberEventSchema = z$1.object({
|
|
484
|
+
version: z$1.literal(2),
|
|
485
|
+
teamId: teamIdSchema,
|
|
486
|
+
member: teamMemberSnapshotSchema
|
|
487
|
+
}).strict();
|
|
488
|
+
const teamTaskEventSchema = z$1.object({
|
|
489
|
+
version: z$1.literal(2),
|
|
490
|
+
teamId: teamIdSchema,
|
|
491
|
+
task: teamTaskSnapshotSchema
|
|
492
|
+
}).strict();
|
|
493
|
+
const teamMessageQueuedEventSchema = z$1.object({
|
|
494
|
+
version: z$1.literal(2),
|
|
495
|
+
teamId: teamIdSchema,
|
|
496
|
+
message: teamMessageSnapshotSchema
|
|
497
|
+
}).strict();
|
|
498
|
+
const teamMessageDeliveredEventSchema = z$1.object({
|
|
499
|
+
version: z$1.literal(2),
|
|
500
|
+
teamId: teamIdSchema,
|
|
501
|
+
messageId: teamMessageIdSchema,
|
|
502
|
+
targetId: sessionIdSchema
|
|
503
|
+
}).strict();
|
|
504
|
+
/**
|
|
505
|
+
* Construct empty state for one Team identity.
|
|
506
|
+
* @param rootId - root Session identity.
|
|
507
|
+
* @returns empty Team state.
|
|
508
|
+
*/
|
|
509
|
+
function emptyTeamState(rootId) {
|
|
510
|
+
return {
|
|
511
|
+
id: TeamId(rootId),
|
|
512
|
+
members: [],
|
|
513
|
+
tasks: [],
|
|
514
|
+
messages: [],
|
|
515
|
+
delivered: [],
|
|
516
|
+
nextTaskNumber: 1
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const teamProjectionEntrySchema = z$1.object({
|
|
520
|
+
id: teamIdSchema,
|
|
521
|
+
members: z$1.array(teamMemberSnapshotSchema),
|
|
522
|
+
tasks: z$1.array(teamTaskSnapshotSchema),
|
|
523
|
+
messages: z$1.array(teamMessageSnapshotSchema),
|
|
524
|
+
delivered: z$1.array(teamMessageIdSchema),
|
|
525
|
+
nextTaskNumber: positiveSafeInteger,
|
|
526
|
+
failure: z$1.string().optional()
|
|
527
|
+
}).strict();
|
|
528
|
+
/**
|
|
529
|
+
* Test whether a Session event belongs to the Team domain.
|
|
530
|
+
* @param event - candidate Session event.
|
|
531
|
+
* @returns whether the event has a Team-owned type.
|
|
532
|
+
*/
|
|
533
|
+
function isTeamEvent(event) {
|
|
534
|
+
return event.type === "team/member" || event.type === "team/task" || event.type === "team/message/queued" || event.type === "team/message/delivered";
|
|
535
|
+
}
|
|
536
|
+
/** Decode one persisted Team value and retain the schema failure as its cause. */
|
|
537
|
+
function parsePersisted(type, schema, value) {
|
|
538
|
+
try {
|
|
539
|
+
return schema.parse(value);
|
|
540
|
+
} catch (error) {
|
|
541
|
+
throw new Error(`persisted Agent Teams ${type} payload is invalid`, { cause: error });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/** Decode the complete current-version payload selected by one Team event type. */
|
|
545
|
+
function parseCurrentTeamEvent(event) {
|
|
546
|
+
switch (event.type) {
|
|
547
|
+
case "team/member": return {
|
|
548
|
+
...event,
|
|
549
|
+
data: parsePersisted(event.type, teamMemberEventSchema, event.data)
|
|
550
|
+
};
|
|
551
|
+
case "team/task": return {
|
|
552
|
+
...event,
|
|
553
|
+
data: parsePersisted(event.type, teamTaskEventSchema, event.data)
|
|
554
|
+
};
|
|
555
|
+
case "team/message/queued": return {
|
|
556
|
+
...event,
|
|
557
|
+
data: parsePersisted(event.type, teamMessageQueuedEventSchema, event.data)
|
|
558
|
+
};
|
|
559
|
+
case "team/message/delivered": return {
|
|
560
|
+
...event,
|
|
561
|
+
data: parsePersisted(event.type, teamMessageDeliveredEventSchema, event.data)
|
|
562
|
+
};
|
|
563
|
+
/* v8 ignore next 2 -- TeamEventType is closed and every member is handled above. */
|
|
564
|
+
default: return event;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function applyProjectionEvent(state, event) {
|
|
568
|
+
if (state.failure !== void 0) return state;
|
|
569
|
+
if (!isTeamEvent(event)) return state;
|
|
570
|
+
try {
|
|
571
|
+
const selector = parsePersisted(event.type, teamEventSelectorSchema, event.data);
|
|
572
|
+
if (selector.teamId !== state.id) return state;
|
|
573
|
+
if (selector.version !== 2) throw new Error(`unsupported Agent Teams event version ${String(selector.version)}`);
|
|
574
|
+
return applyCurrentTeamEvent(state, parseCurrentTeamEvent(event));
|
|
575
|
+
} catch (error) {
|
|
576
|
+
/* v8 ignore next -- the owned Team transition throws Error instances. */
|
|
577
|
+
return {
|
|
578
|
+
...state,
|
|
579
|
+
failure: error instanceof Error ? error.message : String(error)
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function replaceAt(items, index, item) {
|
|
584
|
+
const next = [...items];
|
|
585
|
+
if (index < 0) next.push(item);
|
|
586
|
+
else next[index] = item;
|
|
587
|
+
return next;
|
|
588
|
+
}
|
|
589
|
+
function applyCurrentTeamEvent(state, event) {
|
|
590
|
+
switch (event.type) {
|
|
591
|
+
case "team/member": {
|
|
592
|
+
const member = event.data.member;
|
|
593
|
+
const index = state.members.findIndex((candidate) => candidate.id === member.id);
|
|
594
|
+
const prior = state.members[index];
|
|
595
|
+
const named = state.members.find((candidate) => candidate.name === member.name);
|
|
596
|
+
if (named !== void 0 && named.id !== member.id) throw new Error(`teammate name "${member.name}" is reused by another member`);
|
|
597
|
+
if (prior === void 0) {
|
|
598
|
+
if (member.phase !== "provisioning") throw new Error(`teammate "${member.name}" must begin provisioning`);
|
|
599
|
+
} else {
|
|
600
|
+
if (prior.name !== member.name || prior.provider !== member.provider || prior.context !== member.context) throw new Error(`teammate "${member.id}" changed immutable identity fields`);
|
|
601
|
+
if (prior.phase !== "provisioning" || member.phase === "provisioning") throw new Error(`teammate "${member.name}" has an invalid ${prior.phase} -> ${member.phase} transition`);
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
...state,
|
|
605
|
+
members: replaceAt(state.members, index, member)
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
case "team/task": {
|
|
609
|
+
const task = event.data.task;
|
|
610
|
+
const index = state.tasks.findIndex((candidate) => candidate.id === task.id);
|
|
611
|
+
const prior = state.tasks[index];
|
|
612
|
+
if (prior === void 0 && task.revision !== 1) throw new Error(`team task "${task.id}" must begin at revision 1`);
|
|
613
|
+
if (prior !== void 0 && task.revision !== prior.revision + 1) throw new Error(`team task "${task.id}" revision is not contiguous`);
|
|
614
|
+
assertTaskGraphCandidate(state.tasks, task);
|
|
615
|
+
let nextTaskNumber = state.nextTaskNumber;
|
|
616
|
+
const match = numericTaskIdPattern.exec(task.id);
|
|
617
|
+
if (match !== null) {
|
|
618
|
+
const number = Number(match[1]);
|
|
619
|
+
nextTaskNumber = Math.max(nextTaskNumber, number === Number.MAX_SAFE_INTEGER ? number : number + 1);
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
...state,
|
|
623
|
+
tasks: replaceAt(state.tasks, index, task),
|
|
624
|
+
nextTaskNumber
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
case "team/message/queued": {
|
|
628
|
+
const message = event.data.message;
|
|
629
|
+
if (state.messages.some((candidate) => candidate.id === message.id)) throw new Error(`team message "${message.id}" was queued twice`);
|
|
630
|
+
return {
|
|
631
|
+
...state,
|
|
632
|
+
messages: [...state.messages, message]
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
case "team/message/delivered": {
|
|
636
|
+
const queued = state.messages.find((message) => message.id === event.data.messageId);
|
|
637
|
+
if (queued === void 0) throw new Error(`team message "${event.data.messageId}" was delivered before queueing`);
|
|
638
|
+
if (queued.targetId !== event.data.targetId) throw new Error(`team message "${event.data.messageId}" target changed`);
|
|
639
|
+
if (state.delivered.includes(event.data.messageId)) throw new Error(`team message "${event.data.messageId}" was delivered twice`);
|
|
640
|
+
return {
|
|
641
|
+
...state,
|
|
642
|
+
delivered: [...state.delivered, event.data.messageId]
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/* v8 ignore next 2 -- TeamEventType is closed and every member is handled above. */
|
|
646
|
+
default: return state;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const teamMemberProjectionSchema = z$1.object({
|
|
650
|
+
id: sessionIdSchema,
|
|
651
|
+
name: z$1.string(),
|
|
652
|
+
role: z$1.enum(["lead", "teammate"]),
|
|
653
|
+
phase: z$1.enum([
|
|
654
|
+
"provisioning",
|
|
655
|
+
"active",
|
|
656
|
+
"failed"
|
|
657
|
+
]),
|
|
658
|
+
error: z$1.string().optional()
|
|
659
|
+
}).strict();
|
|
660
|
+
const teamTaskViewSchema = z$1.object({
|
|
661
|
+
id: teamTaskIdSchema,
|
|
662
|
+
revision: positiveSafeInteger,
|
|
663
|
+
subject: z$1.string(),
|
|
664
|
+
description: z$1.string(),
|
|
665
|
+
status: z$1.enum([
|
|
666
|
+
"pending",
|
|
667
|
+
"in_progress",
|
|
668
|
+
"completed",
|
|
669
|
+
"deleted"
|
|
670
|
+
]),
|
|
671
|
+
blockedBy: z$1.array(teamTaskIdSchema),
|
|
672
|
+
writeScopes: z$1.array(z$1.string()),
|
|
673
|
+
ownerName: z$1.string().optional(),
|
|
674
|
+
ready: z$1.boolean(),
|
|
675
|
+
writeScopeWarnings: z$1.array(z$1.string())
|
|
676
|
+
}).strict();
|
|
677
|
+
const teamProjectionSchema = z$1.object({
|
|
678
|
+
members: z$1.array(teamMemberProjectionSchema),
|
|
679
|
+
tasks: z$1.array(teamTaskViewSchema),
|
|
680
|
+
failure: z$1.string().optional()
|
|
681
|
+
}).strict();
|
|
682
|
+
/** Client views keyed by the member and task collections they were derived from. */
|
|
683
|
+
const teamProjectionViews = /* @__PURE__ */ new WeakMap();
|
|
684
|
+
function buildTeamProjection(state) {
|
|
685
|
+
const members = [{
|
|
686
|
+
id: brandString(state.id),
|
|
687
|
+
name: "lead",
|
|
688
|
+
role: "lead",
|
|
689
|
+
phase: "active"
|
|
690
|
+
}];
|
|
691
|
+
for (const member of state.members) members.push({
|
|
692
|
+
id: member.id,
|
|
693
|
+
name: member.name,
|
|
694
|
+
role: "teammate",
|
|
695
|
+
phase: member.phase,
|
|
696
|
+
...member.error === void 0 ? {} : { error: member.error }
|
|
697
|
+
});
|
|
698
|
+
return {
|
|
699
|
+
members,
|
|
700
|
+
tasks: state.tasks.filter((task) => task.status !== "deleted").map((task) => projectTaskView(state, task)),
|
|
701
|
+
...state.failure === void 0 ? {} : { failure: state.failure }
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Durable client view of one Team state. Mailbox-only state changes reuse the
|
|
706
|
+
* previous view reference, so the live drive publishes nothing for them.
|
|
707
|
+
* A failure is terminal: later events retain the failed state reference and
|
|
708
|
+
* do not republish its view.
|
|
709
|
+
* @param state - current Team state.
|
|
710
|
+
* @returns the roster and non-deleted task board, plus any projection failure.
|
|
711
|
+
*/
|
|
712
|
+
function teamProjectionView(state) {
|
|
713
|
+
if (state.failure !== void 0) return buildTeamProjection(state);
|
|
714
|
+
let byTasks = teamProjectionViews.get(state.members);
|
|
715
|
+
if (byTasks === void 0) {
|
|
716
|
+
byTasks = /* @__PURE__ */ new WeakMap();
|
|
717
|
+
teamProjectionViews.set(state.members, byTasks);
|
|
718
|
+
}
|
|
719
|
+
let view = byTasks.get(state.tasks);
|
|
720
|
+
if (view === void 0) {
|
|
721
|
+
view = buildTeamProjection(state);
|
|
722
|
+
byTasks.set(state.tasks, view);
|
|
723
|
+
}
|
|
724
|
+
return view;
|
|
725
|
+
}
|
|
726
|
+
/** Team projection selected by the projected Session identity; the wire view carries durable roster and task state only. */
|
|
727
|
+
const teamProjectionDefinition = {
|
|
728
|
+
key: "agentTeam",
|
|
729
|
+
stateVersion: 4,
|
|
730
|
+
stateSchema: teamProjectionEntrySchema,
|
|
731
|
+
init: (header) => emptyTeamState(header.id),
|
|
732
|
+
apply: applyProjectionEvent,
|
|
733
|
+
wire: {
|
|
734
|
+
viewSchema: teamProjectionSchema,
|
|
735
|
+
view: teamProjectionView
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
//#endregion
|
|
739
|
+
//#region lib/types/remote.js
|
|
740
|
+
/** Wire failure mapping for the Team service's browser write face. */
|
|
741
|
+
/**
|
|
742
|
+
* Map one Team-domain failure onto the shared Remote failure vocabulary so the
|
|
743
|
+
* browser can branch on a stable code. Unknown values pass through unchanged
|
|
744
|
+
* and surface as `gateway/internal`.
|
|
745
|
+
* @param error - the value thrown by the Team domain operation.
|
|
746
|
+
* @returns the value to rethrow from the wire method.
|
|
747
|
+
*/
|
|
748
|
+
function teamWireError(error) {
|
|
749
|
+
if (!(error instanceof TeamError)) return error;
|
|
750
|
+
if (error.code === "TEAM_TASK_STALE_REVISION") return new RemoteError("agent-team/stale-revision", error.message, {});
|
|
751
|
+
if (error.code === "TEAM_NOT_MEMBER") return new RemoteError("agent-team/not-a-member", error.message, {});
|
|
752
|
+
return new RemoteError("agent-team/rejected", error.message, { code: error.code });
|
|
753
|
+
}
|
|
754
|
+
//#endregion
|
|
755
|
+
//#region lib/types/persisted.js
|
|
756
|
+
/** Short-lived read-handle access to persisted Team member Sessions. */
|
|
757
|
+
/**
|
|
758
|
+
* Read one stored session's header and complete event log through a
|
|
759
|
+
* short-lived read handle, closing the handle before returning.
|
|
760
|
+
* @param persistence - the durable session store.
|
|
761
|
+
* @param id - the stored session to read.
|
|
762
|
+
* @param signal - cancellation observed by open and read.
|
|
763
|
+
* @returns the stored header and every committed event.
|
|
764
|
+
*/
|
|
765
|
+
async function readPersistedSession(persistence, id, signal) {
|
|
766
|
+
const handle = await persistence.open(id, "read", { signal });
|
|
767
|
+
try {
|
|
768
|
+
const { events } = await handle.read(0, void 0, { signal });
|
|
769
|
+
return {
|
|
770
|
+
header: handle.header,
|
|
771
|
+
inheritedEventCount: handle.inheritedEventCount,
|
|
772
|
+
events
|
|
773
|
+
};
|
|
774
|
+
} finally {
|
|
775
|
+
await handle.close();
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region lib/types/session-message.js
|
|
780
|
+
/** Durable Session-message acceptance checks shared by provisioning and mailbox recovery. */
|
|
781
|
+
/** Fold the durable inbox suffix into the messages still awaiting a claim. */
|
|
782
|
+
function pendingInboxMessages(events) {
|
|
783
|
+
const inbox = {
|
|
784
|
+
"next-turn": [],
|
|
785
|
+
"next-step": []
|
|
786
|
+
};
|
|
787
|
+
for (const event of events) {
|
|
788
|
+
if (event.type !== "agent/inbox/spliced") continue;
|
|
789
|
+
inbox[event.data.target].splice(event.data.start, event.data.removedCount ?? 0, ...event.data.inserted);
|
|
790
|
+
}
|
|
791
|
+
return [...inbox["next-turn"], ...inbox["next-step"]];
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Test whether one message is model-visible or still durably pending.
|
|
795
|
+
* @param events - one Session's non-inherited event suffix.
|
|
796
|
+
* @param predicate - identity check for the accepted message.
|
|
797
|
+
* @returns whether history or the current inbox contains a match.
|
|
798
|
+
*/
|
|
799
|
+
function messageAccepted(events, predicate) {
|
|
800
|
+
return events.some((event) => event.type === "user/message" && predicate(event.data)) || pendingInboxMessages(events).some(predicate);
|
|
801
|
+
}
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region lib/types/validation.js
|
|
804
|
+
/** Input normalization shared by Team roster and task commands. */
|
|
805
|
+
/**
|
|
806
|
+
* Normalize one required human-authored string.
|
|
807
|
+
* @param value - raw input value.
|
|
808
|
+
* @param field - diagnostic field name.
|
|
809
|
+
* @param maxLength - maximum normalized character count.
|
|
810
|
+
* @returns trimmed non-empty text.
|
|
811
|
+
*/
|
|
812
|
+
function requiredText(value, field, maxLength) {
|
|
813
|
+
const text = value.trim();
|
|
814
|
+
if (text.length === 0) throw new TeamError(`${field} must be non-empty`, "TEAM_INVALID_ARGUMENT");
|
|
815
|
+
if (text.length > maxLength) throw new TeamError(`${field} exceeds ${maxLength} characters`, "TEAM_INVALID_ARGUMENT");
|
|
816
|
+
return text;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Normalize one workspace-relative path prefix without treating it as a lock.
|
|
820
|
+
* @param value - user-authored path prefix.
|
|
821
|
+
* @returns normalized slash-separated prefix.
|
|
822
|
+
*/
|
|
823
|
+
function writeScope(value) {
|
|
824
|
+
const normalized = value.replaceAll("\\", "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
|
|
825
|
+
const segments = normalized.split("/");
|
|
826
|
+
if (normalized.length === 0 || normalized.startsWith("/") || /^[a-z]:/iu.test(normalized) || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) throw new TeamError(`invalid workspace-relative write scope ${JSON.stringify(value)}`, "TEAM_INVALID_WRITE_SCOPE");
|
|
827
|
+
return normalized;
|
|
828
|
+
}
|
|
829
|
+
//#endregion
|
|
830
|
+
//#region lib/types/roster.js
|
|
831
|
+
/** Team membership, continuable-child provisioning, and roster-owned teardown. */
|
|
832
|
+
const MEMBER_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
833
|
+
/**
|
|
834
|
+
* Resolve one active Team member by model-facing name, including the Lead pseudo-row.
|
|
835
|
+
* @param root - exact live Team Lead.
|
|
836
|
+
* @param state - current Team state.
|
|
837
|
+
* @param rawName - candidate member name.
|
|
838
|
+
* @returns resolved durable id and normalized name.
|
|
839
|
+
*/
|
|
840
|
+
function resolveActiveMember(root, state, rawName) {
|
|
841
|
+
const name = rawName.trim();
|
|
842
|
+
if (name === "lead") return {
|
|
843
|
+
id: root.id,
|
|
844
|
+
name
|
|
845
|
+
};
|
|
846
|
+
const member = state.members.find((candidate) => candidate.name === name);
|
|
847
|
+
if (member === void 0 || member.phase !== "active") throw new TeamError(`active teammate "${name}" not found`, "TEAM_MEMBER_NOT_FOUND");
|
|
848
|
+
return {
|
|
849
|
+
id: member.id,
|
|
850
|
+
name
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
/** Owns Team identities and the lifecycle of rostered continuable children. */
|
|
854
|
+
var TeamRoster = class {
|
|
855
|
+
ctx;
|
|
856
|
+
journal;
|
|
857
|
+
lifecycle;
|
|
858
|
+
maxMembers;
|
|
859
|
+
/**
|
|
860
|
+
* @param ctx - Team service context with Agent, Session, persistence, and subagent services.
|
|
861
|
+
* @param journal - authoritative Lead-log transaction owner.
|
|
862
|
+
* @param lifecycle - shared Team runtime admission cutoff.
|
|
863
|
+
* @param maxMembers - maximum immutable roster entries per Team.
|
|
864
|
+
*/
|
|
865
|
+
constructor(ctx, journal, lifecycle, maxMembers) {
|
|
866
|
+
this.ctx = ctx;
|
|
867
|
+
this.journal = journal;
|
|
868
|
+
this.lifecycle = lifecycle;
|
|
869
|
+
this.maxMembers = maxMembers;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Resolve one exact live Agent's Team role.
|
|
873
|
+
* @param agent - exact live Agent used as the authority credential.
|
|
874
|
+
* @returns its root, Team identity, role, and model-facing name.
|
|
875
|
+
*/
|
|
876
|
+
membership(agent) {
|
|
877
|
+
const membership = this.tryMembership(agent);
|
|
878
|
+
if (membership === void 0) throw new TeamError(`agent "${agent.id}" is not a member of an active Agent Team`, "TEAM_NOT_MEMBER");
|
|
879
|
+
return membership;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Resolve a caller without throwing for scoped installation and lifecycle observers.
|
|
883
|
+
* @param agent - candidate exact live Agent.
|
|
884
|
+
* @returns Team membership, or undefined for non-Team subagents and stale identities.
|
|
885
|
+
*/
|
|
886
|
+
tryMembership(agent) {
|
|
887
|
+
if (this.ctx.agents.get(agent.id) !== agent) return void 0;
|
|
888
|
+
try {
|
|
889
|
+
const parentId = agent.session.header.parentSession;
|
|
890
|
+
if (parentId !== void 0) {
|
|
891
|
+
const root = this.ctx.agents.get(parentId);
|
|
892
|
+
if (root !== void 0) {
|
|
893
|
+
const member = this.journal.state(root).members.find((candidate) => candidate.id === agent.id);
|
|
894
|
+
if (member?.phase === "active" || member?.phase === "provisioning") return {
|
|
895
|
+
root,
|
|
896
|
+
id: TeamId(root.id),
|
|
897
|
+
role: "teammate",
|
|
898
|
+
name: member.name
|
|
899
|
+
};
|
|
900
|
+
if (this.subagentDescriptor(agent)) return void 0;
|
|
901
|
+
return {
|
|
902
|
+
root: agent,
|
|
903
|
+
id: TeamId(agent.id),
|
|
904
|
+
role: "lead",
|
|
905
|
+
name: "lead"
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (this.subagentDescriptor(agent)) return void 0;
|
|
910
|
+
return {
|
|
911
|
+
root: agent,
|
|
912
|
+
id: TeamId(agent.id),
|
|
913
|
+
role: "lead",
|
|
914
|
+
name: "lead"
|
|
915
|
+
};
|
|
916
|
+
} catch {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* List the runtime-enriched roster visible to one Team member.
|
|
922
|
+
* @param membership - exact caller membership resolved by this roster.
|
|
923
|
+
* @returns Lead and teammate rows in creation order.
|
|
924
|
+
*/
|
|
925
|
+
list(membership) {
|
|
926
|
+
const { root } = membership;
|
|
927
|
+
const state = this.journal.state(root);
|
|
928
|
+
const result = [{
|
|
929
|
+
id: root.id,
|
|
930
|
+
name: "lead",
|
|
931
|
+
role: "lead",
|
|
932
|
+
status: availability(root),
|
|
933
|
+
...root.options.model === void 0 ? {} : { model: root.options.model },
|
|
934
|
+
diagnostics: []
|
|
935
|
+
}];
|
|
936
|
+
for (const member of state.members) {
|
|
937
|
+
const live = this.ctx.agents.get(member.id);
|
|
938
|
+
const model = live?.options.model ?? root.options.model;
|
|
939
|
+
result.push({
|
|
940
|
+
id: member.id,
|
|
941
|
+
name: member.name,
|
|
942
|
+
role: "teammate",
|
|
943
|
+
status: member.phase === "failed" ? "failed" : member.phase === "provisioning" ? "provisioning" : availability(live),
|
|
944
|
+
description: member.description,
|
|
945
|
+
provider: member.provider,
|
|
946
|
+
context: member.context,
|
|
947
|
+
...model === void 0 ? {} : { model },
|
|
948
|
+
diagnostics: member.error === void 0 ? [] : [member.error]
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
return result;
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Create one named, continuable direct child of the Team Lead.
|
|
955
|
+
* @param caller - exact live Lead Agent.
|
|
956
|
+
* @param request - immutable name, description, prompt, context mode, provider, and cancellation.
|
|
957
|
+
* @returns the active roster row.
|
|
958
|
+
*/
|
|
959
|
+
async spawn(caller, request) {
|
|
960
|
+
if (this.lifecycle.disposed) throw new TeamError("Agent Teams service is disposing", "TEAM_DISPOSED");
|
|
961
|
+
return await this.lifecycle.track(this.spawnAdmitted(caller, request));
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Reconcile provisioning state when one Team member Session starts.
|
|
965
|
+
* @param agent - newly started exact live Agent.
|
|
966
|
+
* @param signal - shared runtime cancellation.
|
|
967
|
+
*/
|
|
968
|
+
async recoverFor(agent, signal) {
|
|
969
|
+
signal.throwIfAborted();
|
|
970
|
+
const membership = this.tryMembership(agent);
|
|
971
|
+
if (membership?.role === "lead") await this.reconcileProvisioning(membership.root, signal);
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Interrupt one live teammate turn without clearing its pending inbox.
|
|
975
|
+
* @param caller - exact live Lead Agent.
|
|
976
|
+
* @param targetName - durable teammate name.
|
|
977
|
+
* @returns the target status sampled before cancellation.
|
|
978
|
+
*/
|
|
979
|
+
interrupt(caller, targetName) {
|
|
980
|
+
const membership = this.membership(caller);
|
|
981
|
+
if (membership.role !== "lead") throw new TeamError("only the Team Lead can interrupt teammates", "TEAM_LEAD_REQUIRED");
|
|
982
|
+
const state = this.journal.state(membership.root);
|
|
983
|
+
const target = resolveActiveMember(membership.root, state, targetName);
|
|
984
|
+
if (target.id === membership.root.id) throw new TeamError("the Team Lead cannot interrupt itself", "TEAM_INVALID_TARGET");
|
|
985
|
+
const live = this.ctx.agents.get(target.id);
|
|
986
|
+
if (live === void 0) return { previousStatus: "inactive" };
|
|
987
|
+
const previousStatus = availability(live);
|
|
988
|
+
this.ctx.subagents.interrupt(target.id, {
|
|
989
|
+
kind: "ancestor",
|
|
990
|
+
agent: caller
|
|
991
|
+
});
|
|
992
|
+
return { previousStatus };
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Group exact live roster children by their current Lead for runtime teardown.
|
|
996
|
+
* @returns each live Lead and the roster child ids currently in the Agent registry.
|
|
997
|
+
*/
|
|
998
|
+
liveChildrenByRoot() {
|
|
999
|
+
const teams = /* @__PURE__ */ new Map();
|
|
1000
|
+
for (const agent of this.ctx.agents.list()) {
|
|
1001
|
+
const rootId = agent.session.header.parentSession;
|
|
1002
|
+
if (rootId === void 0) continue;
|
|
1003
|
+
const root = this.ctx.agents.get(rootId);
|
|
1004
|
+
if (root === void 0 || !this.journal.state(root).members.some((member) => member.id === agent.id)) continue;
|
|
1005
|
+
const children = teams.get(root) ?? [];
|
|
1006
|
+
children.push(agent.id);
|
|
1007
|
+
teams.set(root, children);
|
|
1008
|
+
}
|
|
1009
|
+
return teams;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Release exact teammate Activations through the continuation lifecycle owner.
|
|
1013
|
+
* @param root - exact live Team Lead authorizing release.
|
|
1014
|
+
* @param childIds - selected roster child ids.
|
|
1015
|
+
*/
|
|
1016
|
+
async stopTeammates(root, childIds) {
|
|
1017
|
+
await this.lifecycle.withTimeout(this.ctx.subagents.drainChildren(root, childIds));
|
|
1018
|
+
}
|
|
1019
|
+
/** Perform one creation admitted before the Team runtime disposal cutoff. */
|
|
1020
|
+
async spawnAdmitted(caller, request) {
|
|
1021
|
+
const membership = this.membership(caller);
|
|
1022
|
+
if (membership.role !== "lead") throw new TeamError("only the Team Lead can create teammates", "TEAM_LEAD_REQUIRED");
|
|
1023
|
+
const signal = AbortSignal.any([request.signal, this.lifecycle.signal]);
|
|
1024
|
+
signal.throwIfAborted();
|
|
1025
|
+
const root = membership.root;
|
|
1026
|
+
const name = this.memberName(request.name);
|
|
1027
|
+
const description = requiredText(request.description, "description", 200);
|
|
1028
|
+
const childId = brandString(randomUUID());
|
|
1029
|
+
const member = {
|
|
1030
|
+
id: childId,
|
|
1031
|
+
name,
|
|
1032
|
+
description,
|
|
1033
|
+
provider: requiredText(request.provider, "provider", 200),
|
|
1034
|
+
context: request.context,
|
|
1035
|
+
phase: "provisioning"
|
|
1036
|
+
};
|
|
1037
|
+
await this.journal.transact(root.id, async () => {
|
|
1038
|
+
const state = this.journal.state(root);
|
|
1039
|
+
if (state.members.some((member) => member.name === name)) throw new TeamError(`teammate name "${name}" was already used in this Team`, "TEAM_MEMBER_NAME_TAKEN");
|
|
1040
|
+
if (state.members.length >= this.maxMembers) throw new TeamError(`Team member limit ${this.maxMembers} reached`, "TEAM_MEMBER_LIMIT");
|
|
1041
|
+
await this.journal.appendAndFlush(root, "team/member", {
|
|
1042
|
+
version: 2,
|
|
1043
|
+
teamId: TeamId(root.id),
|
|
1044
|
+
member
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
let started;
|
|
1048
|
+
try {
|
|
1049
|
+
started = await this.ctx.subagents.startActivation({
|
|
1050
|
+
childId,
|
|
1051
|
+
provider: request.provider,
|
|
1052
|
+
label: description,
|
|
1053
|
+
request: {
|
|
1054
|
+
prompt: request.prompt,
|
|
1055
|
+
parent: root
|
|
1056
|
+
},
|
|
1057
|
+
signal,
|
|
1058
|
+
delivery: "parent"
|
|
1059
|
+
});
|
|
1060
|
+
if (started.messageId === void 0) throw new TeamError("the spawned teammate accepted no initial prompt", "TEAM_PROMPT_NOT_ACCEPTED");
|
|
1061
|
+
await this.checkpointInitialPrompt(childId, started.messageId, signal);
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
const failed = {
|
|
1064
|
+
...member,
|
|
1065
|
+
phase: "failed",
|
|
1066
|
+
error: errorMessage(error)
|
|
1067
|
+
};
|
|
1068
|
+
try {
|
|
1069
|
+
const phase = await this.settleProvisioning(root, failed);
|
|
1070
|
+
await this.stopTeammates(root, [childId]);
|
|
1071
|
+
if (phase === "active") throw new TeamError(`teammate "${name}" became active while its creator reported failure`, "TEAM_PROVISIONING_CONFLICT", { cause: error });
|
|
1072
|
+
} catch (recordError) {
|
|
1073
|
+
throw new AggregateError([error, recordError], "teammate creation and durable failure recording both failed");
|
|
1074
|
+
}
|
|
1075
|
+
throw error;
|
|
1076
|
+
}
|
|
1077
|
+
const active = {
|
|
1078
|
+
...member,
|
|
1079
|
+
phase: "active"
|
|
1080
|
+
};
|
|
1081
|
+
if (await this.settleProvisioning(root, active) === "failed") {
|
|
1082
|
+
const conflict = new TeamError(`teammate "${name}" was reconciled as failed while creation was in progress`, "TEAM_PROVISIONING_CONFLICT");
|
|
1083
|
+
try {
|
|
1084
|
+
await this.stopTeammates(root, [childId]);
|
|
1085
|
+
} catch (cleanupError) {
|
|
1086
|
+
/* v8 ignore next -- requires the independently tested HMR settlement conflict and cleanup failure together. */
|
|
1087
|
+
throw new AggregateError([conflict, cleanupError], "provisioning conflict cleanup failed");
|
|
1088
|
+
}
|
|
1089
|
+
throw conflict;
|
|
1090
|
+
}
|
|
1091
|
+
return { member: this.memberView(active) };
|
|
1092
|
+
}
|
|
1093
|
+
/** Flush the accepted initial inbox item before the Lead can commit `active`. */
|
|
1094
|
+
async checkpointInitialPrompt(childId, messageId, signal) {
|
|
1095
|
+
while (true) {
|
|
1096
|
+
signal.throwIfAborted();
|
|
1097
|
+
const session = this.ctx.sessions.get(childId);
|
|
1098
|
+
if (session === void 0) {
|
|
1099
|
+
const stored = await readPersistedSession(this.ctx.sessionPersistence, childId, signal);
|
|
1100
|
+
if (messageAccepted(stored.events.slice(stored.inheritedEventCount), (message) => message.id === messageId)) return;
|
|
1101
|
+
throw new TeamError(`teammate "${childId}" initial prompt was not durably accepted`, "TEAM_PROVISIONING_CONFLICT");
|
|
1102
|
+
}
|
|
1103
|
+
const progress = Promise.withResolvers();
|
|
1104
|
+
progress.promise.catch(() => void 0);
|
|
1105
|
+
const stopEvent = this.ctx.on("session/event", (candidate) => {
|
|
1106
|
+
if (candidate === session) progress.resolve();
|
|
1107
|
+
});
|
|
1108
|
+
const stopDisposed = this.ctx.on("session/disposed", (candidate) => {
|
|
1109
|
+
if (candidate === session) progress.resolve();
|
|
1110
|
+
});
|
|
1111
|
+
const onAbort = () => {
|
|
1112
|
+
const reason = signal.reason;
|
|
1113
|
+
progress.reject(reason instanceof Error ? reason : new TeamError(`teammate creation aborted: ${errorMessage(reason)}`, "TEAM_DISPOSED"));
|
|
1114
|
+
};
|
|
1115
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1116
|
+
try {
|
|
1117
|
+
signal.throwIfAborted();
|
|
1118
|
+
await this.ctx.sessions.flush(session);
|
|
1119
|
+
if (messageAccepted(session.snapshotEvents(session.inheritedEventCount), (message) => message.id === messageId)) return;
|
|
1120
|
+
if (this.ctx.sessions.get(childId) !== session) continue;
|
|
1121
|
+
await progress.promise;
|
|
1122
|
+
} finally {
|
|
1123
|
+
signal.removeEventListener("abort", onAbort);
|
|
1124
|
+
stopDisposed();
|
|
1125
|
+
stopEvent();
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
/** Settle provisioning-only members from their independently durable child Sessions. */
|
|
1130
|
+
async reconcileProvisioning(root, signal) {
|
|
1131
|
+
const provisioning = this.journal.state(root).members.filter((member) => member.phase === "provisioning");
|
|
1132
|
+
for (const member of provisioning) {
|
|
1133
|
+
signal.throwIfAborted();
|
|
1134
|
+
if (this.ctx.agents.get(member.id) !== void 0) continue;
|
|
1135
|
+
let phase = "failed";
|
|
1136
|
+
let failure = "provisioning did not leave a resumable child Session";
|
|
1137
|
+
try {
|
|
1138
|
+
const loaded = await readPersistedSession(this.ctx.sessionPersistence, member.id, signal);
|
|
1139
|
+
const suffix = loaded.events.slice(loaded.inheritedEventCount);
|
|
1140
|
+
const descriptor = foldSubagentDescriptor(suffix);
|
|
1141
|
+
const acceptedInitialPrompt = messageAccepted(suffix, (message) => message.source.kind === "user");
|
|
1142
|
+
if (loaded.header.parentSession === root.id && descriptor?.mode === "continuable" && descriptor.provider === member.provider && acceptedInitialPrompt) phase = "active";
|
|
1143
|
+
else failure = "persisted child Session does not match the provisioned continuation";
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
failure = `child Session recovery failed: ${errorMessage(error)}`;
|
|
1146
|
+
}
|
|
1147
|
+
signal.throwIfAborted();
|
|
1148
|
+
await this.journal.transact(root.id, async () => {
|
|
1149
|
+
signal.throwIfAborted();
|
|
1150
|
+
const current = this.journal.state(root).members.find((candidate) => candidate.id === member.id);
|
|
1151
|
+
if (current?.phase !== "provisioning") return;
|
|
1152
|
+
const settled = {
|
|
1153
|
+
...current,
|
|
1154
|
+
phase,
|
|
1155
|
+
...phase === "failed" ? { error: failure } : {}
|
|
1156
|
+
};
|
|
1157
|
+
await this.journal.appendAndFlush(root, "team/member", {
|
|
1158
|
+
version: 2,
|
|
1159
|
+
teamId: TeamId(root.id),
|
|
1160
|
+
member: settled
|
|
1161
|
+
});
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
/** Build one runtime member row after successful creation. */
|
|
1166
|
+
memberView(member) {
|
|
1167
|
+
const live = this.ctx.agents.get(member.id);
|
|
1168
|
+
return {
|
|
1169
|
+
id: member.id,
|
|
1170
|
+
name: member.name,
|
|
1171
|
+
role: "teammate",
|
|
1172
|
+
status: availability(live),
|
|
1173
|
+
description: member.description,
|
|
1174
|
+
provider: member.provider,
|
|
1175
|
+
context: member.context,
|
|
1176
|
+
...live?.options.model === void 0 ? {} : { model: live.options.model },
|
|
1177
|
+
diagnostics: []
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
/** Validate a never-reused model-facing teammate name. */
|
|
1181
|
+
memberName(value) {
|
|
1182
|
+
if (!MEMBER_NAME.test(value) || value.length > 64 || value === "lead") throw new TeamError("teammate name must be lower-kebab-case, at most 64 characters, and not \"lead\"", "TEAM_INVALID_MEMBER_NAME");
|
|
1183
|
+
return value;
|
|
1184
|
+
}
|
|
1185
|
+
/** Append one terminal provisioning edge unless recovery already settled it. */
|
|
1186
|
+
async settleProvisioning(root, terminal) {
|
|
1187
|
+
return this.journal.transact(root.id, async () => {
|
|
1188
|
+
const current = this.journal.state(root).members.find((member) => member.id === terminal.id);
|
|
1189
|
+
/* v8 ignore next 3 -- the append-only provisioning event is committed by this operation before settlement. */
|
|
1190
|
+
if (current === void 0) throw new TeamError(`provisioned teammate "${terminal.id}" disappeared`, "TEAM_PROVISIONING_CONFLICT");
|
|
1191
|
+
if (current.phase !== "provisioning") return current.phase;
|
|
1192
|
+
await this.journal.appendAndFlush(root, "team/member", {
|
|
1193
|
+
version: 2,
|
|
1194
|
+
teamId: TeamId(root.id),
|
|
1195
|
+
member: terminal
|
|
1196
|
+
});
|
|
1197
|
+
return terminal.phase === "active" ? "active" : "failed";
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
/** Whether a Session's own suffix identifies a provider-owned subagent child. */
|
|
1201
|
+
subagentDescriptor(agent) {
|
|
1202
|
+
return foldSubagentDescriptor(agent.session.snapshotEvents(agent.session.inheritedEventCount)) !== void 0;
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
/** Turn availability is independent of whether the Agent is loaded. */
|
|
1206
|
+
function availability(agent) {
|
|
1207
|
+
return agent?.status === "running" ? "running" : "inactive";
|
|
1208
|
+
}
|
|
1209
|
+
//#endregion
|
|
1210
|
+
//#region lib/types/task-board.js
|
|
1211
|
+
/** Shared Team task DAG commands and runtime-enriched views. */
|
|
1212
|
+
const TASK_GRAPH_ERROR_CODES = {
|
|
1213
|
+
missing: "TEAM_TASK_NOT_FOUND",
|
|
1214
|
+
duplicate: "TEAM_INVALID_ARGUMENT",
|
|
1215
|
+
cycle: "TEAM_TASK_DEPENDENCY_CYCLE"
|
|
1216
|
+
};
|
|
1217
|
+
/** Owns Team task limits, authorization, transitions, and derived views. */
|
|
1218
|
+
var TeamTaskBoard = class {
|
|
1219
|
+
journal;
|
|
1220
|
+
maxTasks;
|
|
1221
|
+
/**
|
|
1222
|
+
* @param journal - authoritative Lead-log transaction owner.
|
|
1223
|
+
* @param maxTasks - maximum non-deleted tasks retained by one Team.
|
|
1224
|
+
*/
|
|
1225
|
+
constructor(journal, maxTasks) {
|
|
1226
|
+
this.journal = journal;
|
|
1227
|
+
this.maxTasks = maxTasks;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Create one unowned pending task in the Team Lead log.
|
|
1231
|
+
* @param membership - exact caller membership resolved by the Team roster.
|
|
1232
|
+
* @param request - task text, blockers, and advisory write scopes.
|
|
1233
|
+
* @returns the revision-one task view.
|
|
1234
|
+
*/
|
|
1235
|
+
async create(membership, request) {
|
|
1236
|
+
const { root } = membership;
|
|
1237
|
+
return this.journal.transact(root.id, async () => {
|
|
1238
|
+
const state = this.journal.state(root);
|
|
1239
|
+
if (state.tasks.filter((task) => task.status !== "deleted").length >= this.maxTasks) throw new TeamError(`Team task limit ${this.maxTasks} reached`, "TEAM_TASK_LIMIT");
|
|
1240
|
+
const id = TeamTaskId(`task-${state.nextTaskNumber}`);
|
|
1241
|
+
if (state.tasks.some((task) => task.id === id)) throw new TeamError("Team task id space exhausted", "TEAM_TASK_LIMIT");
|
|
1242
|
+
const task = {
|
|
1243
|
+
id,
|
|
1244
|
+
revision: 1,
|
|
1245
|
+
subject: requiredText(request.subject, "subject", 200),
|
|
1246
|
+
description: requiredText(request.description, "description", 16384),
|
|
1247
|
+
status: "pending",
|
|
1248
|
+
blockedBy: this.dependencies(request.blockedBy ?? [], state),
|
|
1249
|
+
writeScopes: this.writeScopes(request.writeScopes ?? [])
|
|
1250
|
+
};
|
|
1251
|
+
this.assertTaskGraph(state, task);
|
|
1252
|
+
await this.journal.appendAndFlush(root, "team/task", {
|
|
1253
|
+
version: 2,
|
|
1254
|
+
teamId: TeamId(root.id),
|
|
1255
|
+
task
|
|
1256
|
+
});
|
|
1257
|
+
return projectTaskView(state, task);
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Return one task, including a deleted tombstone.
|
|
1262
|
+
* @param membership - exact caller membership resolved by the Team roster.
|
|
1263
|
+
* @param id - Team-local task identity.
|
|
1264
|
+
* @returns the latest task value and derived readiness diagnostics.
|
|
1265
|
+
*/
|
|
1266
|
+
get(membership, id) {
|
|
1267
|
+
const { root } = membership;
|
|
1268
|
+
const state = this.journal.state(root);
|
|
1269
|
+
const task = state.tasks.find((candidate) => candidate.id === id);
|
|
1270
|
+
if (task === void 0) throw new TeamError(`team task "${id}" not found`, "TEAM_TASK_NOT_FOUND");
|
|
1271
|
+
return projectTaskView(state, task);
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* List current non-deleted tasks in numeric creation order.
|
|
1275
|
+
* @param membership - exact caller membership resolved by the Team roster.
|
|
1276
|
+
* @returns detached current task views.
|
|
1277
|
+
*/
|
|
1278
|
+
list(membership) {
|
|
1279
|
+
const { root } = membership;
|
|
1280
|
+
const state = this.journal.state(root);
|
|
1281
|
+
return state.tasks.filter((task) => task.status !== "deleted").map((task) => projectTaskView(state, task));
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Compare-and-set one authorized task transition.
|
|
1285
|
+
* @param caller - exact live Team member authorizing the mutation.
|
|
1286
|
+
* @param membership - caller role and exact live Lead.
|
|
1287
|
+
* @param request - task identity, expected revision, action, and action fields.
|
|
1288
|
+
* @returns the committed next task revision.
|
|
1289
|
+
*/
|
|
1290
|
+
async update(caller, membership, request) {
|
|
1291
|
+
const root = membership.root;
|
|
1292
|
+
return this.journal.transact(root.id, async () => {
|
|
1293
|
+
const state = this.journal.state(root);
|
|
1294
|
+
const current = state.tasks.find((task) => task.id === request.taskId);
|
|
1295
|
+
if (current === void 0) throw new TeamError(`team task "${request.taskId}" not found`, "TEAM_TASK_NOT_FOUND");
|
|
1296
|
+
if (current.revision !== request.expectedRevision) throw new TeamError(`stale team task "${current.id}" revision ${request.expectedRevision}; current revision is ${current.revision}`, "TEAM_TASK_STALE_REVISION");
|
|
1297
|
+
if (current.status === "deleted") throw new TeamError(`team task "${current.id}" is deleted`, "TEAM_TASK_DELETED");
|
|
1298
|
+
const lead = membership.role === "lead";
|
|
1299
|
+
const owner = current.ownerId === caller.id;
|
|
1300
|
+
const authorizeOwner = () => {
|
|
1301
|
+
if (!lead && !owner) throw new TeamError("task mutation requires its owner or Team Lead", "TEAM_TASK_UNAUTHORIZED");
|
|
1302
|
+
};
|
|
1303
|
+
let next;
|
|
1304
|
+
switch (request.action) {
|
|
1305
|
+
case "claim":
|
|
1306
|
+
if (current.ownerId !== void 0 && current.ownerId !== caller.id) throw new TeamError(`team task "${current.id}" is owned by another member`, "TEAM_TASK_ALREADY_CLAIMED");
|
|
1307
|
+
if (current.status !== "pending" || !taskReady(state, current)) throw new TeamError(`team task "${current.id}" is not ready to claim`, "TEAM_TASK_BLOCKED");
|
|
1308
|
+
next = {
|
|
1309
|
+
...current,
|
|
1310
|
+
status: "in_progress",
|
|
1311
|
+
ownerId: caller.id
|
|
1312
|
+
};
|
|
1313
|
+
break;
|
|
1314
|
+
case "release":
|
|
1315
|
+
authorizeOwner();
|
|
1316
|
+
if (current.status !== "in_progress") throw new TeamError("only an in-progress task can be released", "TEAM_TASK_INVALID_TRANSITION");
|
|
1317
|
+
next = this.withoutOwner({
|
|
1318
|
+
...current,
|
|
1319
|
+
status: "pending"
|
|
1320
|
+
});
|
|
1321
|
+
break;
|
|
1322
|
+
case "edit":
|
|
1323
|
+
authorizeOwner();
|
|
1324
|
+
if (request.subject === void 0 && request.description === void 0 && request.writeScopes === void 0) throw new TeamError("task edit requires subject, description, or write_scopes", "TEAM_INVALID_ARGUMENT");
|
|
1325
|
+
next = {
|
|
1326
|
+
...current,
|
|
1327
|
+
...request.subject === void 0 ? {} : { subject: requiredText(request.subject, "subject", 200) },
|
|
1328
|
+
...request.description === void 0 ? {} : { description: requiredText(request.description, "description", 16384) },
|
|
1329
|
+
...request.writeScopes === void 0 ? {} : { writeScopes: this.writeScopes(request.writeScopes) }
|
|
1330
|
+
};
|
|
1331
|
+
break;
|
|
1332
|
+
case "set_dependencies":
|
|
1333
|
+
authorizeOwner();
|
|
1334
|
+
if (request.blockedBy === void 0) throw new TeamError("set_dependencies requires blocked_by", "TEAM_INVALID_ARGUMENT");
|
|
1335
|
+
next = {
|
|
1336
|
+
...current,
|
|
1337
|
+
blockedBy: this.dependencies(request.blockedBy, state, current.id)
|
|
1338
|
+
};
|
|
1339
|
+
break;
|
|
1340
|
+
case "complete":
|
|
1341
|
+
authorizeOwner();
|
|
1342
|
+
if (current.status !== "in_progress") throw new TeamError("only an in-progress task can complete", "TEAM_TASK_INVALID_TRANSITION");
|
|
1343
|
+
next = {
|
|
1344
|
+
...current,
|
|
1345
|
+
status: "completed"
|
|
1346
|
+
};
|
|
1347
|
+
break;
|
|
1348
|
+
case "reopen":
|
|
1349
|
+
authorizeOwner();
|
|
1350
|
+
if (current.status !== "completed") throw new TeamError("only a completed task can reopen", "TEAM_TASK_INVALID_TRANSITION");
|
|
1351
|
+
next = this.withoutOwner({
|
|
1352
|
+
...current,
|
|
1353
|
+
status: "pending"
|
|
1354
|
+
});
|
|
1355
|
+
break;
|
|
1356
|
+
case "reassign": {
|
|
1357
|
+
if (!lead) throw new TeamError("only the Team Lead can reassign tasks", "TEAM_LEAD_REQUIRED");
|
|
1358
|
+
if (current.status !== "pending" && current.status !== "in_progress") throw new TeamError("only a pending or in-progress task can be reassigned", "TEAM_TASK_INVALID_TRANSITION");
|
|
1359
|
+
if (request.owner === void 0 || request.owner.trim().length === 0) {
|
|
1360
|
+
next = this.withoutOwner({
|
|
1361
|
+
...current,
|
|
1362
|
+
status: "pending"
|
|
1363
|
+
});
|
|
1364
|
+
break;
|
|
1365
|
+
}
|
|
1366
|
+
if (!taskReady(state, current)) throw new TeamError(`team task "${current.id}" is blocked`, "TEAM_TASK_BLOCKED");
|
|
1367
|
+
const assignee = resolveActiveMember(root, state, request.owner);
|
|
1368
|
+
next = {
|
|
1369
|
+
...current,
|
|
1370
|
+
status: "in_progress",
|
|
1371
|
+
ownerId: assignee.id
|
|
1372
|
+
};
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
case "delete": {
|
|
1376
|
+
authorizeOwner();
|
|
1377
|
+
const dependent = state.tasks.find((task) => task.status !== "deleted" && task.id !== current.id && task.blockedBy.includes(current.id));
|
|
1378
|
+
if (dependent !== void 0) throw new TeamError(`team task "${current.id}" still blocks "${dependent.id}"`, "TEAM_TASK_HAS_DEPENDENTS");
|
|
1379
|
+
next = {
|
|
1380
|
+
...current,
|
|
1381
|
+
status: "deleted"
|
|
1382
|
+
};
|
|
1383
|
+
break;
|
|
1384
|
+
}
|
|
1385
|
+
/* v8 ignore next 2 -- TeamTaskAction is closed and every member is handled above. */
|
|
1386
|
+
default: throw new TeamError(`unsupported task action ${String(request.action)}`, "TEAM_INVALID_ARGUMENT");
|
|
1387
|
+
}
|
|
1388
|
+
const task = {
|
|
1389
|
+
...next,
|
|
1390
|
+
revision: current.revision + 1
|
|
1391
|
+
};
|
|
1392
|
+
this.assertTaskGraph(state, task);
|
|
1393
|
+
await this.journal.appendAndFlush(root, "team/task", {
|
|
1394
|
+
version: 2,
|
|
1395
|
+
teamId: TeamId(root.id),
|
|
1396
|
+
task
|
|
1397
|
+
});
|
|
1398
|
+
return projectTaskView(state, task);
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
/** Validate and de-duplicate dependency ids against the current task graph. */
|
|
1402
|
+
dependencies(values, state, self) {
|
|
1403
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1404
|
+
const result = [];
|
|
1405
|
+
for (const id of values) {
|
|
1406
|
+
if (id === self) throw new TeamError("a team task cannot block itself", "TEAM_TASK_DEPENDENCY_CYCLE");
|
|
1407
|
+
if (seen.has(id)) throw new TeamError(`duplicate blocker "${id}"`, "TEAM_INVALID_ARGUMENT");
|
|
1408
|
+
const task = state.tasks.find((candidate) => candidate.id === id);
|
|
1409
|
+
if (task === void 0 || task.status === "deleted") throw new TeamError(`blocker task "${id}" not found`, "TEAM_TASK_NOT_FOUND");
|
|
1410
|
+
seen.add(id);
|
|
1411
|
+
result.push(id);
|
|
1412
|
+
}
|
|
1413
|
+
return result;
|
|
1414
|
+
}
|
|
1415
|
+
/** Normalize and de-duplicate task write scopes. */
|
|
1416
|
+
writeScopes(values) {
|
|
1417
|
+
return [...new Set(values.map(writeScope))];
|
|
1418
|
+
}
|
|
1419
|
+
/** Map shared task-graph validation onto stable command error codes. */
|
|
1420
|
+
assertTaskGraph(state, candidate) {
|
|
1421
|
+
try {
|
|
1422
|
+
assertTaskGraphCandidate(state.tasks, candidate);
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
/* v8 ignore next -- the shared validator is the only statement in the try and throws this exact error. */
|
|
1425
|
+
if (!(error instanceof TeamTaskGraphError)) throw error;
|
|
1426
|
+
throw new TeamError(error.message, TASK_GRAPH_ERROR_CODES[error.violation], { cause: error });
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
/** Remove an optional owner field under exactOptionalPropertyTypes. */
|
|
1430
|
+
withoutOwner(task) {
|
|
1431
|
+
const { ownerId: _ownerId, ...without } = task;
|
|
1432
|
+
return without;
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
//#endregion
|
|
1436
|
+
//#region lib/types/index.js
|
|
1437
|
+
/** Agent Teams service façade over roster, messaging, task, and runtime lifecycle owners. */
|
|
1438
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
1439
|
+
var useValue = arguments.length > 2;
|
|
1440
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
1441
|
+
return useValue ? value : void 0;
|
|
1442
|
+
};
|
|
1443
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
1444
|
+
function accept(f) {
|
|
1445
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
1446
|
+
return f;
|
|
1447
|
+
}
|
|
1448
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
1449
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
1450
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
1451
|
+
var _, done = false;
|
|
1452
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
1453
|
+
var context = {};
|
|
1454
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
1455
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
1456
|
+
context.addInitializer = function(f) {
|
|
1457
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
1458
|
+
extraInitializers.push(accept(f || null));
|
|
1459
|
+
};
|
|
1460
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
1461
|
+
get: descriptor.get,
|
|
1462
|
+
set: descriptor.set
|
|
1463
|
+
} : descriptor[key], context);
|
|
1464
|
+
if (kind === "accessor") {
|
|
1465
|
+
if (result === void 0) continue;
|
|
1466
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
1467
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
1468
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
1469
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
1470
|
+
} else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
|
|
1471
|
+
else descriptor[key] = _;
|
|
1472
|
+
}
|
|
1473
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
1474
|
+
done = true;
|
|
1475
|
+
};
|
|
1476
|
+
const DEFAULT_MAX_MEMBERS = 16;
|
|
1477
|
+
const DEFAULT_MAX_TASKS = 256;
|
|
1478
|
+
const DEFAULT_MAX_MESSAGE_BYTES = 65536;
|
|
1479
|
+
const DEFAULT_DISPOSAL_TIMEOUT_MS = 5e3;
|
|
1480
|
+
/** Validate one positive safe-integer deployment limit. */
|
|
1481
|
+
function positiveLimit(name, value) {
|
|
1482
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new TeamError(`${name} must be a positive safe integer`, "TEAM_INVALID_CONFIG");
|
|
1483
|
+
return value;
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Agent Teams service backed by the exact live Lead Session log. `createTask`
|
|
1487
|
+
* and `updateTask` also carry the browser write bridge through the `agentTeams`
|
|
1488
|
+
* Typert Remote namespace; the wire faces map domain failures onto the shared
|
|
1489
|
+
* Remote failure vocabulary without changing the domain methods' contracts.
|
|
1490
|
+
*/
|
|
1491
|
+
let TeamService = (() => {
|
|
1492
|
+
let _classSuper = TypertRemoteService;
|
|
1493
|
+
let _instanceExtraInitializers = [];
|
|
1494
|
+
let _remoteCreateTask_decorators;
|
|
1495
|
+
let _remoteUpdateTask_decorators;
|
|
1496
|
+
return class TeamService extends _classSuper {
|
|
1497
|
+
static {
|
|
1498
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
1499
|
+
_remoteCreateTask_decorators = [Remote("createTask")];
|
|
1500
|
+
_remoteUpdateTask_decorators = [Remote("updateTask")];
|
|
1501
|
+
__esDecorate(this, null, _remoteCreateTask_decorators, {
|
|
1502
|
+
kind: "method",
|
|
1503
|
+
name: "remoteCreateTask",
|
|
1504
|
+
static: false,
|
|
1505
|
+
private: false,
|
|
1506
|
+
access: {
|
|
1507
|
+
has: (obj) => "remoteCreateTask" in obj,
|
|
1508
|
+
get: (obj) => obj.remoteCreateTask
|
|
1509
|
+
},
|
|
1510
|
+
metadata: _metadata
|
|
1511
|
+
}, null, _instanceExtraInitializers);
|
|
1512
|
+
__esDecorate(this, null, _remoteUpdateTask_decorators, {
|
|
1513
|
+
kind: "method",
|
|
1514
|
+
name: "remoteUpdateTask",
|
|
1515
|
+
static: false,
|
|
1516
|
+
private: false,
|
|
1517
|
+
access: {
|
|
1518
|
+
has: (obj) => "remoteUpdateTask" in obj,
|
|
1519
|
+
get: (obj) => obj.remoteUpdateTask
|
|
1520
|
+
},
|
|
1521
|
+
metadata: _metadata
|
|
1522
|
+
}, null, _instanceExtraInitializers);
|
|
1523
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
1524
|
+
enumerable: true,
|
|
1525
|
+
configurable: true,
|
|
1526
|
+
writable: true,
|
|
1527
|
+
value: _metadata
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
static inject = [
|
|
1531
|
+
"agents",
|
|
1532
|
+
"sessions",
|
|
1533
|
+
"sessionPersistence",
|
|
1534
|
+
"sessionProjections",
|
|
1535
|
+
"subagents"
|
|
1536
|
+
];
|
|
1537
|
+
static Config = z.object({
|
|
1538
|
+
maxMembers: z.number().step(1).min(1).default(DEFAULT_MAX_MEMBERS),
|
|
1539
|
+
maxTasks: z.number().step(1).min(1).default(DEFAULT_MAX_TASKS),
|
|
1540
|
+
maxMessageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_MESSAGE_BYTES),
|
|
1541
|
+
disposalTimeoutMs: z.number().step(1).min(1).default(DEFAULT_DISPOSAL_TIMEOUT_MS)
|
|
1542
|
+
});
|
|
1543
|
+
/** Validated deployment limits used by every Team operation. */
|
|
1544
|
+
config = __runInitializers(this, _instanceExtraInitializers);
|
|
1545
|
+
activity;
|
|
1546
|
+
lifecycle;
|
|
1547
|
+
journal;
|
|
1548
|
+
roster;
|
|
1549
|
+
tasks;
|
|
1550
|
+
constructor(ctx, config = {}) {
|
|
1551
|
+
super(ctx, "agentTeams");
|
|
1552
|
+
this.config = {
|
|
1553
|
+
maxMembers: positiveLimit("maxMembers", config.maxMembers ?? DEFAULT_MAX_MEMBERS),
|
|
1554
|
+
maxTasks: positiveLimit("maxTasks", config.maxTasks ?? DEFAULT_MAX_TASKS),
|
|
1555
|
+
maxMessageBytes: positiveLimit("maxMessageBytes", config.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES),
|
|
1556
|
+
disposalTimeoutMs: positiveLimit("disposalTimeoutMs", config.disposalTimeoutMs ?? DEFAULT_DISPOSAL_TIMEOUT_MS)
|
|
1557
|
+
};
|
|
1558
|
+
this.activity = new TeamActivity();
|
|
1559
|
+
this.lifecycle = new TeamRuntimeLifecycle(this.config.disposalTimeoutMs);
|
|
1560
|
+
this.journal = new TeamJournal(ctx, (root) => {
|
|
1561
|
+
this.activity.notify(TeamId(root.id));
|
|
1562
|
+
});
|
|
1563
|
+
this.roster = new TeamRoster(ctx, this.journal, this.lifecycle, this.config.maxMembers);
|
|
1564
|
+
this.tasks = new TeamTaskBoard(this.journal, this.config.maxTasks);
|
|
1565
|
+
ctx.on("agent/created", ({ agent }) => {
|
|
1566
|
+
this.scheduleRecovery(agent);
|
|
1567
|
+
});
|
|
1568
|
+
ctx.on("agent/status", ({ agent }) => {
|
|
1569
|
+
const membership = this.roster.tryMembership(agent);
|
|
1570
|
+
if (membership !== void 0) this.activity.notify(membership.id);
|
|
1571
|
+
});
|
|
1572
|
+
ctx.effect(() => {
|
|
1573
|
+
const disposeProjection = ctx.root.sessionProjections.register(teamProjectionDefinition);
|
|
1574
|
+
return async () => {
|
|
1575
|
+
try {
|
|
1576
|
+
await this.disposeRuntime();
|
|
1577
|
+
} finally {
|
|
1578
|
+
disposeProjection();
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
}, "agentTeams.runtimeLifecycle()");
|
|
1582
|
+
for (const agent of ctx.agents.list()) this.scheduleRecovery(agent);
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Resolve one exact live Agent's Team role.
|
|
1586
|
+
* @param agent - exact live Agent used as the authority credential.
|
|
1587
|
+
* @returns its root, Team identity, role, and model-facing name.
|
|
1588
|
+
*/
|
|
1589
|
+
membership(agent) {
|
|
1590
|
+
return this.roster.membership(agent);
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* List the runtime-enriched roster visible to one Team member.
|
|
1594
|
+
* @param agent - exact live Team member.
|
|
1595
|
+
* @returns Lead and teammate rows in creation order.
|
|
1596
|
+
*/
|
|
1597
|
+
listMembers(agent) {
|
|
1598
|
+
return this.roster.list(this.roster.membership(agent));
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Create one named, continuable direct child of the Team Lead.
|
|
1602
|
+
* @param caller - exact live Lead Agent.
|
|
1603
|
+
* @param request - immutable name, description, prompt, context mode, provider, and cancellation.
|
|
1604
|
+
* @returns the active roster row.
|
|
1605
|
+
*/
|
|
1606
|
+
async spawnTeammate(caller, request) {
|
|
1607
|
+
return await this.roster.spawn(caller, request);
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Steer one peer message into the target inbox or reject the attempt.
|
|
1611
|
+
* @param caller - exact live sending Team member.
|
|
1612
|
+
* @param request - target name, content, and cancellation before acceptance.
|
|
1613
|
+
* @returns accepted inbox identity; acceptance follows normal Agent persistence and does not await model processing.
|
|
1614
|
+
*/
|
|
1615
|
+
async sendMessage(caller, request) {
|
|
1616
|
+
if (this.lifecycle.disposed) throw new TeamError("Agent Teams service is disposing", "TEAM_DISPOSED");
|
|
1617
|
+
return await this.lifecycle.track(this.sendAdmitted(caller, {
|
|
1618
|
+
...request,
|
|
1619
|
+
signal: AbortSignal.any([request.signal, this.lifecycle.signal])
|
|
1620
|
+
}));
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Admit one peer message and steer it into the target's inbox.
|
|
1624
|
+
* @param caller - exact live sending Team member.
|
|
1625
|
+
* @param request - target name, content, and signal already joined with runtime cancellation.
|
|
1626
|
+
* @returns the durable message identity carried by the delivered source.
|
|
1627
|
+
*/
|
|
1628
|
+
async sendAdmitted(caller, request) {
|
|
1629
|
+
const membership = this.roster.membership(caller);
|
|
1630
|
+
request.signal.throwIfAborted();
|
|
1631
|
+
const { root } = membership;
|
|
1632
|
+
const target = resolveActiveMember(root, this.journal.state(root), request.target);
|
|
1633
|
+
if (target.id === caller.id) throw new TeamError("a Team member cannot message itself", "TEAM_SELF_MESSAGE");
|
|
1634
|
+
const messageId = TeamMessageId(`team-message-${randomUUID()}`);
|
|
1635
|
+
const source = {
|
|
1636
|
+
kind: "team-message",
|
|
1637
|
+
teamId: TeamId(root.id),
|
|
1638
|
+
messageId,
|
|
1639
|
+
senderId: caller.id,
|
|
1640
|
+
senderName: membership.name
|
|
1641
|
+
};
|
|
1642
|
+
const content = [{
|
|
1643
|
+
type: "text",
|
|
1644
|
+
text: `Team message ${messageId} from ${membership.name}:`
|
|
1645
|
+
}, ...structuredClone(request.content)];
|
|
1646
|
+
if (Buffer.byteLength(JSON.stringify(content), "utf8") > this.config.maxMessageBytes) throw new TeamError(`team message exceeds ${this.config.maxMessageBytes} bytes`, "TEAM_MESSAGE_TOO_LARGE");
|
|
1647
|
+
if (target.id === root.id) root.steer(createUserMessage({
|
|
1648
|
+
content,
|
|
1649
|
+
source
|
|
1650
|
+
}));
|
|
1651
|
+
else await steerHostSubagentPrompt(this.ctx.subagents, root, target.id, content, source, request.signal);
|
|
1652
|
+
this.activity.notify(membership.id);
|
|
1653
|
+
return { messageId };
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Create one unowned pending task in the Team Lead log.
|
|
1657
|
+
* @param caller - exact live Team member creating the task.
|
|
1658
|
+
* @param request - task text, blockers, and advisory write scopes.
|
|
1659
|
+
* @returns the revision-one task view.
|
|
1660
|
+
*/
|
|
1661
|
+
async createTask(caller, request) {
|
|
1662
|
+
return await this.tasks.create(this.roster.membership(caller), request);
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Return one task, including a deleted tombstone.
|
|
1666
|
+
* @param caller - exact live Team member reading the task.
|
|
1667
|
+
* @param id - Team-local task identity.
|
|
1668
|
+
* @returns the latest task value and derived readiness diagnostics.
|
|
1669
|
+
*/
|
|
1670
|
+
getTask(caller, id) {
|
|
1671
|
+
return this.tasks.get(this.roster.membership(caller), id);
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* List current non-deleted tasks in numeric creation order.
|
|
1675
|
+
* @param caller - exact live Team member reading the board.
|
|
1676
|
+
* @returns detached current task views.
|
|
1677
|
+
*/
|
|
1678
|
+
listTasks(caller) {
|
|
1679
|
+
return this.tasks.list(this.roster.membership(caller));
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Compare-and-set one authorized task transition.
|
|
1683
|
+
* @param caller - exact live Team member authorizing the mutation.
|
|
1684
|
+
* @param request - task identity, expected revision, action, and action fields.
|
|
1685
|
+
* @returns the committed next task revision.
|
|
1686
|
+
*/
|
|
1687
|
+
async updateTask(caller, request) {
|
|
1688
|
+
return await this.tasks.update(caller, this.roster.membership(caller), request);
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Wire face of {@link TeamService.createTask}: create one shared task on a
|
|
1692
|
+
* person's behalf through the browser write bridge.
|
|
1693
|
+
* @param agent - exact live Team member resolved from the wire identity.
|
|
1694
|
+
* @param request - subject, description, optional blockers, and optional write scopes.
|
|
1695
|
+
* @returns the revision-one task view.
|
|
1696
|
+
* @throws {RemoteError} `agent-team/not-a-member` when the identity is not a Team member, and
|
|
1697
|
+
* `agent-team/rejected` when the domain refuses the request.
|
|
1698
|
+
*/
|
|
1699
|
+
async remoteCreateTask(agent, request) {
|
|
1700
|
+
try {
|
|
1701
|
+
return await this.createTask(agent, request);
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
throw teamWireError(error);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Wire face of {@link TeamService.updateTask}: commit one compare-and-set
|
|
1708
|
+
* task transition on a person's behalf. A committed mutation is durable and
|
|
1709
|
+
* never undone by a later wire cancellation.
|
|
1710
|
+
* @param agent - exact live Team member resolved from the wire identity.
|
|
1711
|
+
* @param request - task identity, expected revision, action, and action fields.
|
|
1712
|
+
* @returns the committed next task view.
|
|
1713
|
+
* @throws {RemoteError} `agent-team/not-a-member` when the identity is not a Team member,
|
|
1714
|
+
* `agent-team/stale-revision` when the expected revision no longer matches, and
|
|
1715
|
+
* `agent-team/rejected` when the domain refuses the transition.
|
|
1716
|
+
*/
|
|
1717
|
+
async remoteUpdateTask(agent, request) {
|
|
1718
|
+
try {
|
|
1719
|
+
return await this.updateTask(agent, request);
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
throw teamWireError(error);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Wait for the next Team-domain or member-status change.
|
|
1726
|
+
* @param caller - exact live Team member waiting for activity.
|
|
1727
|
+
* @param timeoutMs - bounded wait duration from ten seconds through one hour.
|
|
1728
|
+
* @param signal - caller cancellation for the wait only.
|
|
1729
|
+
* @returns one observed change or a timeout result.
|
|
1730
|
+
*/
|
|
1731
|
+
async waitForChange(caller, timeoutMs, signal) {
|
|
1732
|
+
const membership = this.roster.membership(caller);
|
|
1733
|
+
return await this.activity.wait(membership.id, timeoutMs, signal);
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Interrupt one live teammate turn without clearing its pending inbox.
|
|
1737
|
+
* @param caller - exact live Lead Agent.
|
|
1738
|
+
* @param targetName - durable teammate name.
|
|
1739
|
+
* @returns the target status sampled before cancellation.
|
|
1740
|
+
*/
|
|
1741
|
+
interrupt(caller, targetName) {
|
|
1742
|
+
return this.roster.interrupt(caller, targetName);
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Resolve a caller without throwing, used by scoped-tool installation and observers.
|
|
1746
|
+
* @param agent - candidate exact live Agent.
|
|
1747
|
+
* @returns Team membership, or undefined for non-Team subagents and stale identities.
|
|
1748
|
+
*/
|
|
1749
|
+
tryMembership(agent) {
|
|
1750
|
+
return this.roster.tryMembership(agent);
|
|
1751
|
+
}
|
|
1752
|
+
/** Queue one contained recovery pass after publication has unwound. */
|
|
1753
|
+
scheduleRecovery(agent) {
|
|
1754
|
+
queueMicrotask(() => {
|
|
1755
|
+
if (this.lifecycle.disposed) return;
|
|
1756
|
+
this.recoverFor(agent).catch((error) => {
|
|
1757
|
+
if (this.lifecycle.disposed) return;
|
|
1758
|
+
this.ctx.logger.warn(`Agent Teams recovery for "${agent.id}" failed: ${errorMessage(error)}`);
|
|
1759
|
+
});
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
/** Reconcile roster provisioning when one Team member Session starts. */
|
|
1763
|
+
async recoverFor(agent) {
|
|
1764
|
+
await this.roster.recoverFor(agent, this.lifecycle.signal);
|
|
1765
|
+
}
|
|
1766
|
+
/** Stop Team-owned live branches and release every waiter before service disposal completes. */
|
|
1767
|
+
async disposeRuntime() {
|
|
1768
|
+
this.lifecycle.close();
|
|
1769
|
+
this.activity.close();
|
|
1770
|
+
const failures = [];
|
|
1771
|
+
await this.lifecycle.settle(this.lifecycle.pending(), failures);
|
|
1772
|
+
for (const [root, childIds] of this.roster.liveChildrenByRoot()) try {
|
|
1773
|
+
await this.roster.stopTeammates(root, childIds);
|
|
1774
|
+
} catch (error) {
|
|
1775
|
+
failures.push(error);
|
|
1776
|
+
}
|
|
1777
|
+
if (failures.length > 0) throw new AggregateError(failures, "Agent Teams runtime disposal failed");
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
})();
|
|
1781
|
+
//#endregion
|
|
1782
|
+
export { TeamError, TeamId, TeamMessageId, TeamService, TeamService as default, TeamTaskId };
|