@qilin-agent/experimental-agent-team 3.1.3 → 3.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +60 -60
- package/README.md +12 -14
- package/README.zh.md +11 -13
- package/lib/index.js +925 -1145
- package/lib/typert.host.js +9 -9
- package/lib/typert.remote-client.js +2 -2
- package/lib/types/index.d.ts +12 -6
- package/lib/types/index.js +52 -18
- package/lib/types/lifecycle.d.ts +12 -0
- package/lib/types/lifecycle.js +18 -0
- package/lib/types/roster.d.ts +0 -6
- package/lib/types/roster.js +7 -18
- package/lib/types/types.d.ts +3 -6
- package/package.json +22 -22
- package/lib/types/mailbox.d.ts +0 -78
- package/lib/types/mailbox.js +0 -293
package/lib/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import z from "@qilin-agent/schemastery";
|
|
3
|
+
import { HarnessError, createUserMessage } from "@qilin-agent/llm";
|
|
4
|
+
import { steerHostSubagentPrompt } from "@qilin-agent/subagent/internal";
|
|
2
5
|
import { Remote, RemoteError, TypertRemoteService } from "@qilin-agent/typert-protocol";
|
|
3
6
|
import { inspect } from "node:util";
|
|
4
|
-
import {
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { z as z$1 } from "zod";
|
|
6
8
|
import { brandString } from "@qilin-agent/brand";
|
|
7
|
-
import { steerHostSubagentPrompt } from "@qilin-agent/subagent/internal";
|
|
8
9
|
import { foldSubagentDescriptor } from "@qilin-agent/subagent";
|
|
9
|
-
import { z as z$1 } from "zod";
|
|
10
10
|
//#region lib/types/error.js
|
|
11
11
|
/** Typed Agent Teams failures. */
|
|
12
12
|
/** Stable failure raised by the Team domain. */
|
|
@@ -165,6 +165,7 @@ var TeamJournal = class {
|
|
|
165
165
|
/** Owns the single Team runtime cancellation fact and disposal timeout. */
|
|
166
166
|
var TeamRuntimeLifecycle = class {
|
|
167
167
|
disposalTimeoutMs;
|
|
168
|
+
operations = /* @__PURE__ */ new Set();
|
|
168
169
|
controller = new AbortController();
|
|
169
170
|
/**
|
|
170
171
|
* @param disposalTimeoutMs - maximum wait for one disposal settlement operation.
|
|
@@ -202,6 +203,27 @@ var TeamRuntimeLifecycle = class {
|
|
|
202
203
|
this.controller.abort(new TeamError("Agent Teams service disposed", "TEAM_DISPOSED"));
|
|
203
204
|
}
|
|
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
|
+
/**
|
|
205
227
|
* Await admitted operations and retain failures other than runtime cancellation.
|
|
206
228
|
* @param operations - admitted operations captured after the admission cutoff.
|
|
207
229
|
* @param failures - aggregate destination for unexpected rejection or timeout.
|
|
@@ -235,54 +257,6 @@ var TeamRuntimeLifecycle = class {
|
|
|
235
257
|
}
|
|
236
258
|
};
|
|
237
259
|
//#endregion
|
|
238
|
-
//#region lib/types/persisted.js
|
|
239
|
-
/** Short-lived read-handle access to persisted Team member Sessions. */
|
|
240
|
-
/**
|
|
241
|
-
* Read one stored session's header and complete event log through a
|
|
242
|
-
* short-lived read handle, closing the handle before returning.
|
|
243
|
-
* @param persistence - the durable session store.
|
|
244
|
-
* @param id - the stored session to read.
|
|
245
|
-
* @param signal - cancellation observed by open and read.
|
|
246
|
-
* @returns the stored header and every committed event.
|
|
247
|
-
*/
|
|
248
|
-
async function readPersistedSession(persistence, id, signal) {
|
|
249
|
-
const handle = await persistence.open(id, "read", { signal });
|
|
250
|
-
try {
|
|
251
|
-
const { events } = await handle.read(0, void 0, { signal });
|
|
252
|
-
return {
|
|
253
|
-
header: handle.header,
|
|
254
|
-
inheritedEventCount: handle.inheritedEventCount,
|
|
255
|
-
events
|
|
256
|
-
};
|
|
257
|
-
} finally {
|
|
258
|
-
await handle.close();
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
//#endregion
|
|
262
|
-
//#region lib/types/session-message.js
|
|
263
|
-
/** Durable Session-message acceptance checks shared by provisioning and mailbox recovery. */
|
|
264
|
-
/** Fold the durable inbox suffix into the messages still awaiting a claim. */
|
|
265
|
-
function pendingInboxMessages(events) {
|
|
266
|
-
const inbox = {
|
|
267
|
-
"next-turn": [],
|
|
268
|
-
"next-step": []
|
|
269
|
-
};
|
|
270
|
-
for (const event of events) {
|
|
271
|
-
if (event.type !== "agent/inbox/spliced") continue;
|
|
272
|
-
inbox[event.data.target].splice(event.data.start, event.data.removedCount ?? 0, ...event.data.inserted);
|
|
273
|
-
}
|
|
274
|
-
return [...inbox["next-turn"], ...inbox["next-step"]];
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Test whether one message is model-visible or still durably pending.
|
|
278
|
-
* @param events - one Session's non-inherited event suffix.
|
|
279
|
-
* @param predicate - identity check for the accepted message.
|
|
280
|
-
* @returns whether history or the current inbox contains a match.
|
|
281
|
-
*/
|
|
282
|
-
function messageAccepted(events, predicate) {
|
|
283
|
-
return events.some((event) => event.type === "user/message" && predicate(event.data)) || pendingInboxMessages(events).some(predicate);
|
|
284
|
-
}
|
|
285
|
-
//#endregion
|
|
286
260
|
//#region lib/types/types.js
|
|
287
261
|
/** Public Agent Teams identities, durable records, and service request values. */
|
|
288
262
|
/**
|
|
@@ -310,1148 +284,927 @@ function TeamMessageId(id) {
|
|
|
310
284
|
return id;
|
|
311
285
|
}
|
|
312
286
|
//#endregion
|
|
313
|
-
//#region lib/types/
|
|
314
|
-
/**
|
|
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
|
+
};
|
|
315
302
|
/**
|
|
316
|
-
*
|
|
317
|
-
* @param
|
|
318
|
-
* @param
|
|
319
|
-
* @
|
|
320
|
-
* @returns trimmed non-empty text.
|
|
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.
|
|
321
307
|
*/
|
|
322
|
-
function
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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);
|
|
327
335
|
}
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region lib/types/task-view.js
|
|
338
|
+
/** Pure task-view derivation shared by the task board and the client projection. */
|
|
328
339
|
/**
|
|
329
|
-
*
|
|
330
|
-
* @param
|
|
331
|
-
* @
|
|
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.
|
|
332
344
|
*/
|
|
333
|
-
function
|
|
334
|
-
|
|
335
|
-
const segments = normalized.split("/");
|
|
336
|
-
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");
|
|
337
|
-
return normalized;
|
|
345
|
+
function scopesOverlap(left, right) {
|
|
346
|
+
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
338
347
|
}
|
|
339
|
-
//#endregion
|
|
340
|
-
//#region lib/types/roster.js
|
|
341
|
-
/** Team membership, continuable-child provisioning, and roster-owned teardown. */
|
|
342
|
-
const MEMBER_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
343
348
|
/**
|
|
344
|
-
*
|
|
345
|
-
* @param
|
|
346
|
-
* @param
|
|
347
|
-
* @
|
|
348
|
-
* @returns resolved durable id and normalized name.
|
|
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.
|
|
349
353
|
*/
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
+
}
|
|
358
373
|
return {
|
|
359
|
-
id:
|
|
360
|
-
|
|
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]
|
|
361
384
|
};
|
|
362
385
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
return
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
* Group exact live roster children by their current Lead for runtime teardown.
|
|
520
|
-
* @returns each live Lead and the roster child ids currently in the Agent registry.
|
|
521
|
-
*/
|
|
522
|
-
liveChildrenByRoot() {
|
|
523
|
-
const teams = /* @__PURE__ */ new Map();
|
|
524
|
-
for (const agent of this.ctx.agents.list()) {
|
|
525
|
-
const rootId = agent.session.header.parentSession;
|
|
526
|
-
if (rootId === void 0) continue;
|
|
527
|
-
const root = this.ctx.agents.get(rootId);
|
|
528
|
-
if (root === void 0 || !this.journal.state(root).members.some((member) => member.id === agent.id)) continue;
|
|
529
|
-
const children = teams.get(root) ?? [];
|
|
530
|
-
children.push(agent.id);
|
|
531
|
-
teams.set(root, children);
|
|
532
|
-
}
|
|
533
|
-
return teams;
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* Release exact teammate Activations through the continuation lifecycle owner.
|
|
537
|
-
* @param root - exact live Team Lead authorizing release.
|
|
538
|
-
* @param childIds - selected roster child ids.
|
|
539
|
-
*/
|
|
540
|
-
async stopTeammates(root, childIds) {
|
|
541
|
-
await this.lifecycle.withTimeout(this.ctx.subagents.drainContinuableChildren(root, childIds));
|
|
542
|
-
}
|
|
543
|
-
/** Perform one creation admitted before the Team runtime disposal cutoff. */
|
|
544
|
-
async spawnAdmitted(caller, request) {
|
|
545
|
-
const membership = this.membership(caller);
|
|
546
|
-
if (membership.role !== "lead") throw new TeamError("only the Team Lead can create teammates", "TEAM_LEAD_REQUIRED");
|
|
547
|
-
const signal = AbortSignal.any([request.signal, this.lifecycle.signal]);
|
|
548
|
-
signal.throwIfAborted();
|
|
549
|
-
const root = membership.root;
|
|
550
|
-
const name = this.memberName(request.name);
|
|
551
|
-
const description = requiredText(request.description, "description", 200);
|
|
552
|
-
const childId = brandString(randomUUID());
|
|
553
|
-
const member = {
|
|
554
|
-
id: childId,
|
|
555
|
-
name,
|
|
556
|
-
description,
|
|
557
|
-
provider: requiredText(request.provider, "provider", 200),
|
|
558
|
-
context: request.context,
|
|
559
|
-
phase: "provisioning"
|
|
560
|
-
};
|
|
561
|
-
await this.journal.transact(root.id, async () => {
|
|
562
|
-
const state = this.journal.state(root);
|
|
563
|
-
if (state.members.some((member) => member.name === name)) throw new TeamError(`teammate name "${name}" was already used in this Team`, "TEAM_MEMBER_NAME_TAKEN");
|
|
564
|
-
if (state.members.length >= this.maxMembers) throw new TeamError(`Team member limit ${this.maxMembers} reached`, "TEAM_MEMBER_LIMIT");
|
|
565
|
-
await this.journal.appendAndFlush(root, "team/member", {
|
|
566
|
-
version: 2,
|
|
567
|
-
teamId: TeamId(root.id),
|
|
568
|
-
member
|
|
569
|
-
});
|
|
570
|
-
});
|
|
571
|
-
let started;
|
|
572
|
-
try {
|
|
573
|
-
started = await this.ctx.subagents.startContinuable({
|
|
574
|
-
childId,
|
|
575
|
-
provider: request.provider,
|
|
576
|
-
label: description,
|
|
577
|
-
request: {
|
|
578
|
-
prompt: request.prompt,
|
|
579
|
-
parent: root
|
|
580
|
-
},
|
|
581
|
-
signal
|
|
582
|
-
});
|
|
583
|
-
await this.checkpointInitialPrompt(childId, started.messageId, signal);
|
|
584
|
-
} catch (error) {
|
|
585
|
-
const failed = {
|
|
586
|
-
...member,
|
|
587
|
-
phase: "failed",
|
|
588
|
-
error: errorMessage(error)
|
|
589
|
-
};
|
|
590
|
-
try {
|
|
591
|
-
const phase = await this.settleProvisioning(root, failed);
|
|
592
|
-
await this.stopTeammates(root, [childId]);
|
|
593
|
-
if (phase === "active") throw new TeamError(`teammate "${name}" became active while its creator reported failure`, "TEAM_PROVISIONING_CONFLICT", { cause: error });
|
|
594
|
-
} catch (recordError) {
|
|
595
|
-
throw new AggregateError([error, recordError], "teammate creation and durable failure recording both failed");
|
|
596
|
-
}
|
|
597
|
-
throw error;
|
|
598
|
-
}
|
|
599
|
-
const active = {
|
|
600
|
-
...member,
|
|
601
|
-
phase: "active"
|
|
602
|
-
};
|
|
603
|
-
if (await this.settleProvisioning(root, active) === "failed") {
|
|
604
|
-
const conflict = new TeamError(`teammate "${name}" was reconciled as failed while creation was in progress`, "TEAM_PROVISIONING_CONFLICT");
|
|
605
|
-
try {
|
|
606
|
-
await this.stopTeammates(root, [childId]);
|
|
607
|
-
} catch (cleanupError) {
|
|
608
|
-
/* v8 ignore next -- requires the independently tested HMR settlement conflict and cleanup failure together. */
|
|
609
|
-
throw new AggregateError([conflict, cleanupError], "provisioning conflict cleanup failed");
|
|
610
|
-
}
|
|
611
|
-
throw conflict;
|
|
612
|
-
}
|
|
613
|
-
return { member: this.memberView(active) };
|
|
614
|
-
}
|
|
615
|
-
/** Flush the accepted initial inbox item before the Lead can commit `active`. */
|
|
616
|
-
async checkpointInitialPrompt(childId, messageId, signal) {
|
|
617
|
-
while (true) {
|
|
618
|
-
signal.throwIfAborted();
|
|
619
|
-
const session = this.ctx.sessions.get(childId);
|
|
620
|
-
if (session === void 0) {
|
|
621
|
-
const stored = await readPersistedSession(this.ctx.sessionPersistence, childId, signal);
|
|
622
|
-
if (messageAccepted(stored.events.slice(stored.inheritedEventCount), (message) => message.id === messageId)) return;
|
|
623
|
-
throw new TeamError(`teammate "${childId}" initial prompt was not durably accepted`, "TEAM_PROVISIONING_CONFLICT");
|
|
624
|
-
}
|
|
625
|
-
const progress = Promise.withResolvers();
|
|
626
|
-
progress.promise.catch(() => void 0);
|
|
627
|
-
const stopEvent = this.ctx.on("session/event", (candidate) => {
|
|
628
|
-
if (candidate === session) progress.resolve();
|
|
629
|
-
});
|
|
630
|
-
const stopDisposed = this.ctx.on("session/disposed", (candidate) => {
|
|
631
|
-
if (candidate === session) progress.resolve();
|
|
632
|
-
});
|
|
633
|
-
const onAbort = () => {
|
|
634
|
-
const reason = signal.reason;
|
|
635
|
-
progress.reject(reason instanceof Error ? reason : new TeamError(`teammate creation aborted: ${errorMessage(reason)}`, "TEAM_DISPOSED"));
|
|
636
|
-
};
|
|
637
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
638
|
-
try {
|
|
639
|
-
signal.throwIfAborted();
|
|
640
|
-
await this.ctx.sessions.flush(session);
|
|
641
|
-
if (messageAccepted(session.snapshotEvents(session.inheritedEventCount), (message) => message.id === messageId)) return;
|
|
642
|
-
if (this.ctx.sessions.get(childId) !== session) continue;
|
|
643
|
-
await progress.promise;
|
|
644
|
-
} finally {
|
|
645
|
-
signal.removeEventListener("abort", onAbort);
|
|
646
|
-
stopDisposed();
|
|
647
|
-
stopEvent();
|
|
648
|
-
}
|
|
649
|
-
}
|
|
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 });
|
|
650
542
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (current?.phase !== "provisioning") return;
|
|
674
|
-
const settled = {
|
|
675
|
-
...current,
|
|
676
|
-
phase,
|
|
677
|
-
...phase === "failed" ? { error: failure } : {}
|
|
678
|
-
};
|
|
679
|
-
await this.journal.appendAndFlush(root, "team/member", {
|
|
680
|
-
version: 2,
|
|
681
|
-
teamId: TeamId(root.id),
|
|
682
|
-
member: settled
|
|
683
|
-
});
|
|
684
|
-
});
|
|
685
|
-
}
|
|
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;
|
|
686
565
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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. */
|
|
690
577
|
return {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
role: "teammate",
|
|
694
|
-
status: availability(live),
|
|
695
|
-
description: member.description,
|
|
696
|
-
provider: member.provider,
|
|
697
|
-
context: member.context,
|
|
698
|
-
...live?.options.model === void 0 ? {} : { model: live.options.model },
|
|
699
|
-
diagnostics: []
|
|
578
|
+
...state,
|
|
579
|
+
failure: error instanceof Error ? error.message : String(error)
|
|
700
580
|
};
|
|
701
581
|
}
|
|
702
|
-
/** Validate a never-reused model-facing teammate name. */
|
|
703
|
-
memberName(value) {
|
|
704
|
-
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");
|
|
705
|
-
return value;
|
|
706
|
-
}
|
|
707
|
-
/** Append one terminal provisioning edge unless recovery already settled it. */
|
|
708
|
-
async settleProvisioning(root, terminal) {
|
|
709
|
-
return this.journal.transact(root.id, async () => {
|
|
710
|
-
const current = this.journal.state(root).members.find((member) => member.id === terminal.id);
|
|
711
|
-
/* v8 ignore next 3 -- the append-only provisioning event is committed by this operation before settlement. */
|
|
712
|
-
if (current === void 0) throw new TeamError(`provisioned teammate "${terminal.id}" disappeared`, "TEAM_PROVISIONING_CONFLICT");
|
|
713
|
-
if (current.phase !== "provisioning") return current.phase;
|
|
714
|
-
await this.journal.appendAndFlush(root, "team/member", {
|
|
715
|
-
version: 2,
|
|
716
|
-
teamId: TeamId(root.id),
|
|
717
|
-
member: terminal
|
|
718
|
-
});
|
|
719
|
-
return terminal.phase === "active" ? "active" : "failed";
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
/** Whether a Session's own suffix identifies a provider-owned subagent child. */
|
|
723
|
-
subagentDescriptor(agent) {
|
|
724
|
-
return foldSubagentDescriptor(agent.session.snapshotEvents(agent.session.inheritedEventCount)) !== void 0;
|
|
725
|
-
}
|
|
726
|
-
};
|
|
727
|
-
/** Turn availability is independent of whether the Agent is loaded. */
|
|
728
|
-
function availability(agent) {
|
|
729
|
-
return agent?.status === "running" ? "running" : "inactive";
|
|
730
582
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
this.ctx = ctx;
|
|
755
|
-
this.journal = journal;
|
|
756
|
-
this.roster = roster;
|
|
757
|
-
this.lifecycle = lifecycle;
|
|
758
|
-
this.maxPendingMessagesPerMember = maxPendingMessagesPerMember;
|
|
759
|
-
this.maxMessageBytes = maxMessageBytes;
|
|
760
|
-
}
|
|
761
|
-
/**
|
|
762
|
-
* Queue one durable peer message, then attempt immediate delivery.
|
|
763
|
-
* @param caller - exact live sending Team member.
|
|
764
|
-
* @param request - target name, content, and pre-queue cancellation.
|
|
765
|
-
* @returns durable message identity and immediate-delivery observation.
|
|
766
|
-
*/
|
|
767
|
-
async send(caller, request) {
|
|
768
|
-
if (this.lifecycle.disposed) throw new TeamError("Agent Teams service is disposing", "TEAM_DISPOSED");
|
|
769
|
-
const operation = this.sendAdmitted(caller, {
|
|
770
|
-
...request,
|
|
771
|
-
signal: AbortSignal.any([request.signal, this.lifecycle.signal])
|
|
772
|
-
});
|
|
773
|
-
return await this.trackDispatch(operation);
|
|
774
|
-
}
|
|
775
|
-
/**
|
|
776
|
-
* Observe target-side durable receipts and checkpoint their Lead-log acknowledgement.
|
|
777
|
-
* @param session - exact target Session receiving the event.
|
|
778
|
-
* @param event - newly appended Session event.
|
|
779
|
-
*/
|
|
780
|
-
observeSessionEvent(session, event) {
|
|
781
|
-
if (this.lifecycle.disposed || event.type !== "user/message" || event.data.source.kind !== "team-message") return;
|
|
782
|
-
const source = event.data.source;
|
|
783
|
-
const acknowledgement = Promise.resolve().then(async () => {
|
|
784
|
-
const root = this.ctx.agents.get(brandString(source.teamId));
|
|
785
|
-
if (root !== void 0) await this.checkpointDelivered(root, session, source.messageId);
|
|
786
|
-
}).catch((error) => {
|
|
787
|
-
this.ctx.logger.warn(`Team message "${source.messageId}" acknowledgement failed: ${errorMessage(error)}`);
|
|
788
|
-
});
|
|
789
|
-
this.trackDispatch(acknowledgement);
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* Retry durable pending messages relevant to one started Team member.
|
|
793
|
-
* @param agent - newly started exact live Agent.
|
|
794
|
-
* @param signal - shared runtime cancellation.
|
|
795
|
-
*/
|
|
796
|
-
async recoverFor(agent, signal) {
|
|
797
|
-
signal.throwIfAborted();
|
|
798
|
-
const membership = this.roster.tryMembership(agent);
|
|
799
|
-
if (membership === void 0) return;
|
|
800
|
-
const state = this.journal.state(membership.root);
|
|
801
|
-
const messages = state.messages.filter((message) => !state.delivered.includes(message.id) && (membership.role === "lead" || message.targetId === agent.id));
|
|
802
|
-
for (const message of messages) {
|
|
803
|
-
signal.throwIfAborted();
|
|
804
|
-
await this.tryDispatch(membership.root, message, signal);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
/**
|
|
808
|
-
* Return admitted dispatch and acknowledgement operations captured for disposal.
|
|
809
|
-
* @returns detached snapshot ordered only by Set insertion.
|
|
810
|
-
*/
|
|
811
|
-
pendingDispatches() {
|
|
812
|
-
return [...this.inFlightDispatches];
|
|
813
|
-
}
|
|
814
|
-
/** Queue and dispatch one mailbox item admitted before the disposal cutoff. */
|
|
815
|
-
async sendAdmitted(caller, request) {
|
|
816
|
-
const membership = this.roster.membership(caller);
|
|
817
|
-
request.signal.throwIfAborted();
|
|
818
|
-
const root = membership.root;
|
|
819
|
-
const content = structuredClone(request.content);
|
|
820
|
-
const queued = await this.journal.transact(root.id, async () => {
|
|
821
|
-
request.signal.throwIfAborted();
|
|
822
|
-
const state = this.journal.state(root);
|
|
823
|
-
const target = resolveActiveMember(root, state, request.target);
|
|
824
|
-
if (target.id === caller.id) throw new TeamError("a Team member cannot message itself", "TEAM_SELF_MESSAGE");
|
|
825
|
-
const pendingForTarget = state.messages.filter((candidate) => candidate.targetId === target.id && !state.delivered.includes(candidate.id)).length;
|
|
826
|
-
if (pendingForTarget >= this.maxPendingMessagesPerMember) throw new TeamError(`teammate "${target.name}" has ${pendingForTarget} pending messages`, "TEAM_MAILBOX_FULL");
|
|
827
|
-
const queued = {
|
|
828
|
-
id: TeamMessageId(`team-message-${randomUUID()}`),
|
|
829
|
-
senderId: caller.id,
|
|
830
|
-
senderName: membership.name,
|
|
831
|
-
targetId: target.id,
|
|
832
|
-
content
|
|
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)
|
|
833
606
|
};
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
});
|
|
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
|
+
}
|
|
840
621
|
return {
|
|
841
|
-
|
|
842
|
-
|
|
622
|
+
...state,
|
|
623
|
+
tasks: replaceAt(state.tasks, index, task),
|
|
624
|
+
nextTaskNumber
|
|
843
625
|
};
|
|
844
|
-
});
|
|
845
|
-
const accepted = await queued.dispatch;
|
|
846
|
-
return {
|
|
847
|
-
messageId: queued.message.id,
|
|
848
|
-
status: accepted ? "accepted" : "queued"
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
/** Attempt one queued message exactly once in this process at a time. */
|
|
852
|
-
tryDispatch(root, message, signal) {
|
|
853
|
-
if (this.lifecycle.disposed) return Promise.resolve(false);
|
|
854
|
-
if (this.inFlightMessages.has(message.id)) return Promise.resolve(false);
|
|
855
|
-
this.inFlightMessages.add(message.id);
|
|
856
|
-
const operation = this.trackDispatch(this.tryDispatchAdmitted(root, message, AbortSignal.any([signal, this.lifecycle.signal])));
|
|
857
|
-
const forget = () => {
|
|
858
|
-
this.inFlightMessages.delete(message.id);
|
|
859
|
-
};
|
|
860
|
-
operation.then(forget, forget);
|
|
861
|
-
return operation;
|
|
862
|
-
}
|
|
863
|
-
/** Track one dispatch transaction through delivery admission or contained failure. */
|
|
864
|
-
trackDispatch(operation) {
|
|
865
|
-
this.inFlightDispatches.add(operation);
|
|
866
|
-
operation.then(() => {
|
|
867
|
-
this.inFlightDispatches.delete(operation);
|
|
868
|
-
}, () => {
|
|
869
|
-
this.inFlightDispatches.delete(operation);
|
|
870
|
-
});
|
|
871
|
-
return operation;
|
|
872
|
-
}
|
|
873
|
-
/** Attempt one queued message admitted before the service lifecycle cutoff. */
|
|
874
|
-
async tryDispatchAdmitted(root, message, signal) {
|
|
875
|
-
return await this.serializeDispatch(message, () => this.dispatchThrough(root, message, signal));
|
|
876
|
-
}
|
|
877
|
-
/** Serialize delivery admission for one durable target in queued order. */
|
|
878
|
-
async serializeDispatch(message, operation) {
|
|
879
|
-
const targetId = message.targetId;
|
|
880
|
-
/* v8 ignore next -- dispatch tails absorb rejection, so the recovery callback is a fail-safe backstop. */
|
|
881
|
-
const run = (this.dispatchTails.get(targetId) ?? Promise.resolve()).then(operation, operation);
|
|
882
|
-
/* v8 ignore next -- dispatchOnce contains delivery failures and serializeDispatch itself does not throw. */
|
|
883
|
-
const tail = run.then(() => void 0, () => void 0);
|
|
884
|
-
this.dispatchTails.set(targetId, tail);
|
|
885
|
-
try {
|
|
886
|
-
return await run;
|
|
887
|
-
} finally {
|
|
888
|
-
if (this.dispatchTails.get(targetId) === tail) this.dispatchTails.delete(targetId);
|
|
889
626
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
for (const candidate of pending.slice(0, requested + 1)) {
|
|
898
|
-
const ownsInFlight = !this.inFlightMessages.has(candidate.id);
|
|
899
|
-
if (ownsInFlight) this.inFlightMessages.add(candidate.id);
|
|
900
|
-
try {
|
|
901
|
-
if (!await this.dispatchOnce(root, candidate, signal)) return false;
|
|
902
|
-
} finally {
|
|
903
|
-
if (ownsInFlight) this.inFlightMessages.delete(candidate.id);
|
|
904
|
-
}
|
|
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
|
+
};
|
|
905
634
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
kind: "team-message",
|
|
915
|
-
teamId: TeamId(root.id),
|
|
916
|
-
messageId: message.id,
|
|
917
|
-
senderId: message.senderId,
|
|
918
|
-
senderName: message.senderName
|
|
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]
|
|
919
643
|
};
|
|
920
|
-
const content = this.deliveryContent(message);
|
|
921
|
-
if (message.targetId === root.id) {
|
|
922
|
-
const input = createUserMessage({
|
|
923
|
-
content,
|
|
924
|
-
source
|
|
925
|
-
});
|
|
926
|
-
root.steer(input);
|
|
927
|
-
return await this.checkpointDelivered(root, root.session, message.id);
|
|
928
|
-
}
|
|
929
|
-
if (target === void 0) {
|
|
930
|
-
const recorded = await this.persistedTargetRecorded(message.targetId, message.id, signal);
|
|
931
|
-
if (recorded === void 0) return false;
|
|
932
|
-
if (recorded) {
|
|
933
|
-
await this.markDelivered(root, message.id, message.targetId);
|
|
934
|
-
return true;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
await steerHostSubagentPrompt(this.ctx.subagents, root, message.targetId, content, source, signal);
|
|
938
|
-
return target === void 0 ? true : await this.checkpointDelivered(root, target.session, message.id);
|
|
939
|
-
} catch (error) {
|
|
940
|
-
this.ctx.logger.warn(`team message "${message.id}" remains queued: ${errorMessage(error)}`);
|
|
941
|
-
return false;
|
|
942
644
|
}
|
|
645
|
+
/* v8 ignore next 2 -- TeamEventType is closed and every member is handled above. */
|
|
646
|
+
default: return state;
|
|
943
647
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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);
|
|
969
718
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
text: `Team message ${message.id} from ${message.senderName}:`
|
|
975
|
-
}, ...structuredClone(message.content)];
|
|
719
|
+
let view = byTasks.get(state.tasks);
|
|
720
|
+
if (view === void 0) {
|
|
721
|
+
view = buildTeamProjection(state);
|
|
722
|
+
byTasks.set(state.tasks, view);
|
|
976
723
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
|
986
736
|
}
|
|
987
737
|
};
|
|
988
738
|
//#endregion
|
|
989
|
-
//#region lib/types/
|
|
990
|
-
/**
|
|
991
|
-
/** Package-private task dependency failure retained for command error mapping. */
|
|
992
|
-
var TeamTaskGraphError = class extends Error {
|
|
993
|
-
violation;
|
|
994
|
-
/**
|
|
995
|
-
* @param message - concrete invalid dependency relation.
|
|
996
|
-
* @param violation - stable relation category used by Team commands.
|
|
997
|
-
*/
|
|
998
|
-
constructor(message, violation) {
|
|
999
|
-
super(message);
|
|
1000
|
-
this.violation = violation;
|
|
1001
|
-
this.name = "TeamTaskGraphError";
|
|
1002
|
-
}
|
|
1003
|
-
};
|
|
739
|
+
//#region lib/types/remote.js
|
|
740
|
+
/** Wire failure mapping for the Team service's browser write face. */
|
|
1004
741
|
/**
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
* @
|
|
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.
|
|
1009
747
|
*/
|
|
1010
|
-
function
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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();
|
|
1023
776
|
}
|
|
1024
|
-
const visiting = /* @__PURE__ */ new Set();
|
|
1025
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1026
|
-
const visit = (id) => {
|
|
1027
|
-
if (visiting.has(id)) throw new TeamTaskGraphError(`task dependency cycle includes "${id}"`, "cycle");
|
|
1028
|
-
if (visited.has(id)) return;
|
|
1029
|
-
const task = tasks.get(id);
|
|
1030
|
-
if (task === void 0 || task.status === "deleted") return;
|
|
1031
|
-
visiting.add(id);
|
|
1032
|
-
for (const blockerId of task.blockedBy) visit(blockerId);
|
|
1033
|
-
visiting.delete(id);
|
|
1034
|
-
visited.add(id);
|
|
1035
|
-
};
|
|
1036
|
-
for (const task of tasks.values()) visit(task.id);
|
|
1037
777
|
}
|
|
1038
778
|
//#endregion
|
|
1039
|
-
//#region lib/types/
|
|
1040
|
-
/**
|
|
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
|
+
}
|
|
1041
793
|
/**
|
|
1042
|
-
*
|
|
1043
|
-
* @param
|
|
1044
|
-
* @param
|
|
1045
|
-
* @returns whether
|
|
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.
|
|
1046
798
|
*/
|
|
1047
|
-
function
|
|
1048
|
-
return
|
|
799
|
+
function messageAccepted(events, predicate) {
|
|
800
|
+
return events.some((event) => event.type === "user/message" && predicate(event.data)) || pendingInboxMessages(events).some(predicate);
|
|
1049
801
|
}
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region lib/types/validation.js
|
|
804
|
+
/** Input normalization shared by Team roster and task commands. */
|
|
1050
805
|
/**
|
|
1051
|
-
*
|
|
1052
|
-
* @param
|
|
1053
|
-
* @param
|
|
1054
|
-
* @
|
|
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.
|
|
1055
811
|
*/
|
|
1056
|
-
function
|
|
1057
|
-
|
|
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;
|
|
1058
817
|
}
|
|
1059
818
|
/**
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
* do not change when that snapshot is appended.
|
|
1064
|
-
* @param state - Team state supplying members and sibling tasks.
|
|
1065
|
-
* @param task - durable task snapshot to view.
|
|
1066
|
-
* @returns a detached task view.
|
|
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.
|
|
1067
822
|
*/
|
|
1068
|
-
function
|
|
1069
|
-
const
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
if (task.writeScopes.some((left) => other.writeScopes.some((right) => scopesOverlap(left, right)))) warnings.add(`write scopes overlap with ${other.id}`);
|
|
1074
|
-
}
|
|
1075
|
-
return {
|
|
1076
|
-
id: task.id,
|
|
1077
|
-
revision: task.revision,
|
|
1078
|
-
subject: task.subject,
|
|
1079
|
-
description: task.description,
|
|
1080
|
-
status: task.status,
|
|
1081
|
-
blockedBy: [...task.blockedBy],
|
|
1082
|
-
writeScopes: [...task.writeScopes],
|
|
1083
|
-
...ownerName === void 0 ? {} : { ownerName },
|
|
1084
|
-
ready: task.status === "pending" && taskReady(state, task),
|
|
1085
|
-
writeScopeWarnings: [...warnings]
|
|
1086
|
-
};
|
|
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;
|
|
1087
828
|
}
|
|
1088
829
|
//#endregion
|
|
1089
|
-
//#region lib/types/
|
|
1090
|
-
/** Team
|
|
1091
|
-
const
|
|
1092
|
-
const positiveSafeInteger = nonNegativeSafeInteger.min(1);
|
|
1093
|
-
const sessionIdSchema = z$1.string().min(1).transform((value) => brandString(value));
|
|
1094
|
-
const teamIdSchema = z$1.string().min(1).transform((value) => TeamId(value));
|
|
1095
|
-
const numericTaskIdPattern = /^task-(\d+)$/u;
|
|
1096
|
-
const teamTaskIdSchema = z$1.string().min(1).refine((value) => {
|
|
1097
|
-
const match = numericTaskIdPattern.exec(value);
|
|
1098
|
-
return match === null || Number.isSafeInteger(Number(match[1]));
|
|
1099
|
-
}, { message: "numeric task id suffix must be a safe integer" }).transform((value) => TeamTaskId(value));
|
|
1100
|
-
const teamMessageIdSchema = z$1.string().min(1).transform((value) => TeamMessageId(value));
|
|
1101
|
-
const coreContentBlockTypes = new Set([
|
|
1102
|
-
"text",
|
|
1103
|
-
"reasoning",
|
|
1104
|
-
"image",
|
|
1105
|
-
"tool-call",
|
|
1106
|
-
"tool-result"
|
|
1107
|
-
]);
|
|
1108
|
-
const imageAttachmentSchema = z$1.object({
|
|
1109
|
-
attachmentId: z$1.string().min(1),
|
|
1110
|
-
mediaType: z$1.enum([
|
|
1111
|
-
"image/png",
|
|
1112
|
-
"image/jpeg",
|
|
1113
|
-
"image/webp",
|
|
1114
|
-
"image/gif"
|
|
1115
|
-
]),
|
|
1116
|
-
bytes: nonNegativeSafeInteger,
|
|
1117
|
-
width: positiveSafeInteger,
|
|
1118
|
-
height: positiveSafeInteger,
|
|
1119
|
-
name: z$1.string().optional()
|
|
1120
|
-
}).strict();
|
|
1121
|
-
const contentBlockSchema = z$1.lazy(() => z$1.union([
|
|
1122
|
-
z$1.object({
|
|
1123
|
-
type: z$1.literal("text"),
|
|
1124
|
-
text: z$1.string()
|
|
1125
|
-
}).strict(),
|
|
1126
|
-
z$1.object({
|
|
1127
|
-
type: z$1.literal("reasoning"),
|
|
1128
|
-
text: z$1.string()
|
|
1129
|
-
}).strict(),
|
|
1130
|
-
z$1.object({
|
|
1131
|
-
type: z$1.literal("image"),
|
|
1132
|
-
attachment: imageAttachmentSchema
|
|
1133
|
-
}).strict(),
|
|
1134
|
-
z$1.object({
|
|
1135
|
-
type: z$1.literal("tool-call"),
|
|
1136
|
-
id: z$1.string().min(1),
|
|
1137
|
-
name: z$1.string(),
|
|
1138
|
-
arguments: z$1.string()
|
|
1139
|
-
}).strict(),
|
|
1140
|
-
z$1.custom((value) => {
|
|
1141
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1142
|
-
const type = value.type;
|
|
1143
|
-
return typeof type === "string" && type.length > 0 && !coreContentBlockTypes.has(type);
|
|
1144
|
-
})
|
|
1145
|
-
]));
|
|
1146
|
-
const teamMemberSnapshotSchema = z$1.object({
|
|
1147
|
-
id: sessionIdSchema,
|
|
1148
|
-
name: z$1.string(),
|
|
1149
|
-
description: z$1.string(),
|
|
1150
|
-
provider: z$1.string(),
|
|
1151
|
-
context: z$1.enum(["fresh", "fork"]),
|
|
1152
|
-
phase: z$1.enum([
|
|
1153
|
-
"provisioning",
|
|
1154
|
-
"active",
|
|
1155
|
-
"failed"
|
|
1156
|
-
]),
|
|
1157
|
-
error: z$1.string().optional()
|
|
1158
|
-
}).strict();
|
|
1159
|
-
const teamTaskSnapshotSchema = z$1.object({
|
|
1160
|
-
id: teamTaskIdSchema,
|
|
1161
|
-
revision: positiveSafeInteger,
|
|
1162
|
-
subject: z$1.string(),
|
|
1163
|
-
description: z$1.string(),
|
|
1164
|
-
status: z$1.enum([
|
|
1165
|
-
"pending",
|
|
1166
|
-
"in_progress",
|
|
1167
|
-
"completed",
|
|
1168
|
-
"deleted"
|
|
1169
|
-
]),
|
|
1170
|
-
ownerId: sessionIdSchema.optional(),
|
|
1171
|
-
blockedBy: z$1.array(teamTaskIdSchema),
|
|
1172
|
-
writeScopes: z$1.array(z$1.string())
|
|
1173
|
-
}).strict();
|
|
1174
|
-
const teamMessageSnapshotSchema = z$1.object({
|
|
1175
|
-
id: teamMessageIdSchema,
|
|
1176
|
-
senderId: sessionIdSchema,
|
|
1177
|
-
senderName: z$1.string(),
|
|
1178
|
-
targetId: sessionIdSchema,
|
|
1179
|
-
content: z$1.array(contentBlockSchema)
|
|
1180
|
-
}).strict();
|
|
1181
|
-
const teamEventSelectorSchema = z$1.object({
|
|
1182
|
-
version: nonNegativeSafeInteger,
|
|
1183
|
-
teamId: teamIdSchema
|
|
1184
|
-
}).loose();
|
|
1185
|
-
const teamMemberEventSchema = z$1.object({
|
|
1186
|
-
version: z$1.literal(2),
|
|
1187
|
-
teamId: teamIdSchema,
|
|
1188
|
-
member: teamMemberSnapshotSchema
|
|
1189
|
-
}).strict();
|
|
1190
|
-
const teamTaskEventSchema = z$1.object({
|
|
1191
|
-
version: z$1.literal(2),
|
|
1192
|
-
teamId: teamIdSchema,
|
|
1193
|
-
task: teamTaskSnapshotSchema
|
|
1194
|
-
}).strict();
|
|
1195
|
-
const teamMessageQueuedEventSchema = z$1.object({
|
|
1196
|
-
version: z$1.literal(2),
|
|
1197
|
-
teamId: teamIdSchema,
|
|
1198
|
-
message: teamMessageSnapshotSchema
|
|
1199
|
-
}).strict();
|
|
1200
|
-
const teamMessageDeliveredEventSchema = z$1.object({
|
|
1201
|
-
version: z$1.literal(2),
|
|
1202
|
-
teamId: teamIdSchema,
|
|
1203
|
-
messageId: teamMessageIdSchema,
|
|
1204
|
-
targetId: sessionIdSchema
|
|
1205
|
-
}).strict();
|
|
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;
|
|
1206
833
|
/**
|
|
1207
|
-
*
|
|
1208
|
-
* @param
|
|
1209
|
-
* @
|
|
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.
|
|
1210
839
|
*/
|
|
1211
|
-
function
|
|
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");
|
|
1212
848
|
return {
|
|
1213
|
-
id:
|
|
1214
|
-
|
|
1215
|
-
tasks: [],
|
|
1216
|
-
messages: [],
|
|
1217
|
-
delivered: [],
|
|
1218
|
-
nextTaskNumber: 1
|
|
849
|
+
id: member.id,
|
|
850
|
+
name
|
|
1219
851
|
};
|
|
1220
852
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
*
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
/** Decode one persisted Team value and retain the schema failure as its cause. */
|
|
1239
|
-
function parsePersisted(type, schema, value) {
|
|
1240
|
-
try {
|
|
1241
|
-
return schema.parse(value);
|
|
1242
|
-
} catch (error) {
|
|
1243
|
-
throw new Error(`persisted Agent Teams ${type} payload is invalid`, { cause: error });
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
/** Decode the complete current-version payload selected by one Team event type. */
|
|
1247
|
-
function parseCurrentTeamEvent(event) {
|
|
1248
|
-
switch (event.type) {
|
|
1249
|
-
case "team/member": return {
|
|
1250
|
-
...event,
|
|
1251
|
-
data: parsePersisted(event.type, teamMemberEventSchema, event.data)
|
|
1252
|
-
};
|
|
1253
|
-
case "team/task": return {
|
|
1254
|
-
...event,
|
|
1255
|
-
data: parsePersisted(event.type, teamTaskEventSchema, event.data)
|
|
1256
|
-
};
|
|
1257
|
-
case "team/message/queued": return {
|
|
1258
|
-
...event,
|
|
1259
|
-
data: parsePersisted(event.type, teamMessageQueuedEventSchema, event.data)
|
|
1260
|
-
};
|
|
1261
|
-
case "team/message/delivered": return {
|
|
1262
|
-
...event,
|
|
1263
|
-
data: parsePersisted(event.type, teamMessageDeliveredEventSchema, event.data)
|
|
1264
|
-
};
|
|
1265
|
-
/* v8 ignore next 2 -- TeamEventType is closed and every member is handled above. */
|
|
1266
|
-
default: return event;
|
|
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;
|
|
1267
870
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
if (
|
|
1276
|
-
return
|
|
1277
|
-
} catch (error) {
|
|
1278
|
-
/* v8 ignore next -- the owned Team transition throws Error instances. */
|
|
1279
|
-
return {
|
|
1280
|
-
...state,
|
|
1281
|
-
failure: error instanceof Error ? error.message : String(error)
|
|
1282
|
-
};
|
|
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;
|
|
1283
880
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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
|
+
}
|
|
1304
908
|
}
|
|
909
|
+
if (this.subagentDescriptor(agent)) return void 0;
|
|
1305
910
|
return {
|
|
1306
|
-
|
|
1307
|
-
|
|
911
|
+
root: agent,
|
|
912
|
+
id: TeamId(agent.id),
|
|
913
|
+
role: "lead",
|
|
914
|
+
name: "lead"
|
|
1308
915
|
};
|
|
916
|
+
} catch {
|
|
917
|
+
return;
|
|
1309
918
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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");
|
|
1322
1088
|
}
|
|
1323
|
-
|
|
1324
|
-
...state,
|
|
1325
|
-
tasks: replaceAt(state.tasks, index, task),
|
|
1326
|
-
nextTaskNumber
|
|
1327
|
-
};
|
|
1089
|
+
throw conflict;
|
|
1328
1090
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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"));
|
|
1335
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
|
+
}
|
|
1336
1127
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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
|
+
});
|
|
1346
1163
|
}
|
|
1347
|
-
/* v8 ignore next 2 -- TeamEventType is closed and every member is handled above. */
|
|
1348
|
-
default: return state;
|
|
1349
1164
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
revision: positiveSafeInteger,
|
|
1365
|
-
subject: z$1.string(),
|
|
1366
|
-
description: z$1.string(),
|
|
1367
|
-
status: z$1.enum([
|
|
1368
|
-
"pending",
|
|
1369
|
-
"in_progress",
|
|
1370
|
-
"completed",
|
|
1371
|
-
"deleted"
|
|
1372
|
-
]),
|
|
1373
|
-
blockedBy: z$1.array(teamTaskIdSchema),
|
|
1374
|
-
writeScopes: z$1.array(z$1.string()),
|
|
1375
|
-
ownerName: z$1.string().optional(),
|
|
1376
|
-
ready: z$1.boolean(),
|
|
1377
|
-
writeScopeWarnings: z$1.array(z$1.string())
|
|
1378
|
-
}).strict();
|
|
1379
|
-
const teamProjectionSchema = z$1.object({
|
|
1380
|
-
members: z$1.array(teamMemberProjectionSchema),
|
|
1381
|
-
tasks: z$1.array(teamTaskViewSchema),
|
|
1382
|
-
failure: z$1.string().optional()
|
|
1383
|
-
}).strict();
|
|
1384
|
-
/** Client views keyed by the member and task collections they were derived from. */
|
|
1385
|
-
const teamProjectionViews = /* @__PURE__ */ new WeakMap();
|
|
1386
|
-
function buildTeamProjection(state) {
|
|
1387
|
-
const members = [{
|
|
1388
|
-
id: brandString(state.id),
|
|
1389
|
-
name: "lead",
|
|
1390
|
-
role: "lead",
|
|
1391
|
-
phase: "active"
|
|
1392
|
-
}];
|
|
1393
|
-
for (const member of state.members) members.push({
|
|
1394
|
-
id: member.id,
|
|
1395
|
-
name: member.name,
|
|
1396
|
-
role: "teammate",
|
|
1397
|
-
phase: member.phase,
|
|
1398
|
-
...member.error === void 0 ? {} : { error: member.error }
|
|
1399
|
-
});
|
|
1400
|
-
return {
|
|
1401
|
-
members,
|
|
1402
|
-
tasks: state.tasks.filter((task) => task.status !== "deleted").map((task) => projectTaskView(state, task)),
|
|
1403
|
-
...state.failure === void 0 ? {} : { failure: state.failure }
|
|
1404
|
-
};
|
|
1405
|
-
}
|
|
1406
|
-
/**
|
|
1407
|
-
* Durable client view of one Team state. Mailbox-only state changes reuse the
|
|
1408
|
-
* previous view reference, so the live drive publishes nothing for them.
|
|
1409
|
-
* A failure is terminal: later events retain the failed state reference and
|
|
1410
|
-
* do not republish its view.
|
|
1411
|
-
* @param state - current Team state.
|
|
1412
|
-
* @returns the roster and non-deleted task board, plus any projection failure.
|
|
1413
|
-
*/
|
|
1414
|
-
function teamProjectionView(state) {
|
|
1415
|
-
if (state.failure !== void 0) return buildTeamProjection(state);
|
|
1416
|
-
let byTasks = teamProjectionViews.get(state.members);
|
|
1417
|
-
if (byTasks === void 0) {
|
|
1418
|
-
byTasks = /* @__PURE__ */ new WeakMap();
|
|
1419
|
-
teamProjectionViews.set(state.members, byTasks);
|
|
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
|
+
};
|
|
1420
1179
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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;
|
|
1425
1184
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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;
|
|
1438
1203
|
}
|
|
1439
1204
|
};
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
/**
|
|
1444
|
-
* Map one Team-domain failure onto the shared Remote failure vocabulary so the
|
|
1445
|
-
* browser can branch on a stable code. Unknown values pass through unchanged
|
|
1446
|
-
* and surface as `gateway/internal`.
|
|
1447
|
-
* @param error - the value thrown by the Team domain operation.
|
|
1448
|
-
* @returns the value to rethrow from the wire method.
|
|
1449
|
-
*/
|
|
1450
|
-
function teamWireError(error) {
|
|
1451
|
-
if (!(error instanceof TeamError)) return error;
|
|
1452
|
-
if (error.code === "TEAM_TASK_STALE_REVISION") return new RemoteError("agent-team/stale-revision", error.message, {});
|
|
1453
|
-
if (error.code === "TEAM_NOT_MEMBER") return new RemoteError("agent-team/not-a-member", error.message, {});
|
|
1454
|
-
return new RemoteError("agent-team/rejected", error.message, { code: error.code });
|
|
1205
|
+
/** Turn availability is independent of whether the Agent is loaded. */
|
|
1206
|
+
function availability(agent) {
|
|
1207
|
+
return agent?.status === "running" ? "running" : "inactive";
|
|
1455
1208
|
}
|
|
1456
1209
|
//#endregion
|
|
1457
1210
|
//#region lib/types/task-board.js
|
|
@@ -1681,7 +1434,7 @@ var TeamTaskBoard = class {
|
|
|
1681
1434
|
};
|
|
1682
1435
|
//#endregion
|
|
1683
1436
|
//#region lib/types/index.js
|
|
1684
|
-
/** Agent Teams service façade over roster,
|
|
1437
|
+
/** Agent Teams service façade over roster, messaging, task, and runtime lifecycle owners. */
|
|
1685
1438
|
var __runInitializers = function(thisArg, initializers, value) {
|
|
1686
1439
|
var useValue = arguments.length > 2;
|
|
1687
1440
|
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
@@ -1722,7 +1475,6 @@ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializ
|
|
|
1722
1475
|
};
|
|
1723
1476
|
const DEFAULT_MAX_MEMBERS = 16;
|
|
1724
1477
|
const DEFAULT_MAX_TASKS = 256;
|
|
1725
|
-
const DEFAULT_MAX_PENDING_MESSAGES = 64;
|
|
1726
1478
|
const DEFAULT_MAX_MESSAGE_BYTES = 65536;
|
|
1727
1479
|
const DEFAULT_DISPOSAL_TIMEOUT_MS = 5e3;
|
|
1728
1480
|
/** Validate one positive safe-integer deployment limit. */
|
|
@@ -1785,7 +1537,6 @@ let TeamService = (() => {
|
|
|
1785
1537
|
static Config = z.object({
|
|
1786
1538
|
maxMembers: z.number().step(1).min(1).default(DEFAULT_MAX_MEMBERS),
|
|
1787
1539
|
maxTasks: z.number().step(1).min(1).default(DEFAULT_MAX_TASKS),
|
|
1788
|
-
maxPendingMessagesPerMember: z.number().step(1).min(1).default(DEFAULT_MAX_PENDING_MESSAGES),
|
|
1789
1540
|
maxMessageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_MESSAGE_BYTES),
|
|
1790
1541
|
disposalTimeoutMs: z.number().step(1).min(1).default(DEFAULT_DISPOSAL_TIMEOUT_MS)
|
|
1791
1542
|
});
|
|
@@ -1795,14 +1546,12 @@ let TeamService = (() => {
|
|
|
1795
1546
|
lifecycle;
|
|
1796
1547
|
journal;
|
|
1797
1548
|
roster;
|
|
1798
|
-
mailbox;
|
|
1799
1549
|
tasks;
|
|
1800
1550
|
constructor(ctx, config = {}) {
|
|
1801
1551
|
super(ctx, "agentTeams");
|
|
1802
1552
|
this.config = {
|
|
1803
1553
|
maxMembers: positiveLimit("maxMembers", config.maxMembers ?? DEFAULT_MAX_MEMBERS),
|
|
1804
1554
|
maxTasks: positiveLimit("maxTasks", config.maxTasks ?? DEFAULT_MAX_TASKS),
|
|
1805
|
-
maxPendingMessagesPerMember: positiveLimit("maxPendingMessagesPerMember", config.maxPendingMessagesPerMember ?? DEFAULT_MAX_PENDING_MESSAGES),
|
|
1806
1555
|
maxMessageBytes: positiveLimit("maxMessageBytes", config.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES),
|
|
1807
1556
|
disposalTimeoutMs: positiveLimit("disposalTimeoutMs", config.disposalTimeoutMs ?? DEFAULT_DISPOSAL_TIMEOUT_MS)
|
|
1808
1557
|
};
|
|
@@ -1812,11 +1561,7 @@ let TeamService = (() => {
|
|
|
1812
1561
|
this.activity.notify(TeamId(root.id));
|
|
1813
1562
|
});
|
|
1814
1563
|
this.roster = new TeamRoster(ctx, this.journal, this.lifecycle, this.config.maxMembers);
|
|
1815
|
-
this.mailbox = new TeamMailbox(ctx, this.journal, this.roster, this.lifecycle, this.config.maxPendingMessagesPerMember, this.config.maxMessageBytes);
|
|
1816
1564
|
this.tasks = new TeamTaskBoard(this.journal, this.config.maxTasks);
|
|
1817
|
-
ctx.on("session/event", (session, event) => {
|
|
1818
|
-
this.mailbox.observeSessionEvent(session, event);
|
|
1819
|
-
});
|
|
1820
1565
|
ctx.on("agent/created", ({ agent }) => {
|
|
1821
1566
|
this.scheduleRecovery(agent);
|
|
1822
1567
|
});
|
|
@@ -1862,13 +1607,50 @@ let TeamService = (() => {
|
|
|
1862
1607
|
return await this.roster.spawn(caller, request);
|
|
1863
1608
|
}
|
|
1864
1609
|
/**
|
|
1865
|
-
*
|
|
1610
|
+
* Steer one peer message into the target inbox or reject the attempt.
|
|
1866
1611
|
* @param caller - exact live sending Team member.
|
|
1867
|
-
* @param request - target name, content, and
|
|
1868
|
-
* @returns
|
|
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.
|
|
1869
1614
|
*/
|
|
1870
1615
|
async sendMessage(caller, request) {
|
|
1871
|
-
|
|
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 };
|
|
1872
1654
|
}
|
|
1873
1655
|
/**
|
|
1874
1656
|
* Create one unowned pending task in the Team Lead log.
|
|
@@ -1977,18 +1759,16 @@ let TeamService = (() => {
|
|
|
1977
1759
|
});
|
|
1978
1760
|
});
|
|
1979
1761
|
}
|
|
1980
|
-
/** Reconcile roster provisioning
|
|
1762
|
+
/** Reconcile roster provisioning when one Team member Session starts. */
|
|
1981
1763
|
async recoverFor(agent) {
|
|
1982
1764
|
await this.roster.recoverFor(agent, this.lifecycle.signal);
|
|
1983
|
-
await this.mailbox.recoverFor(agent, this.lifecycle.signal);
|
|
1984
1765
|
}
|
|
1985
1766
|
/** Stop Team-owned live branches and release every waiter before service disposal completes. */
|
|
1986
1767
|
async disposeRuntime() {
|
|
1987
1768
|
this.lifecycle.close();
|
|
1988
1769
|
this.activity.close();
|
|
1989
1770
|
const failures = [];
|
|
1990
|
-
await this.lifecycle.settle(this.
|
|
1991
|
-
await this.lifecycle.settle(this.mailbox.pendingDispatches(), failures);
|
|
1771
|
+
await this.lifecycle.settle(this.lifecycle.pending(), failures);
|
|
1992
1772
|
for (const [root, childIds] of this.roster.liveChildrenByRoot()) try {
|
|
1993
1773
|
await this.roster.stopTeammates(root, childIds);
|
|
1994
1774
|
} catch (error) {
|