@deepseek-ai/dsh-experimental-agent-team 0.1.5-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +220 -0
  4. package/README.zh.md +220 -0
  5. package/lib/index.js +1894 -0
  6. package/lib/invariant.js +361 -0
  7. package/lib/typert.host.d.ts +3 -0
  8. package/lib/typert.host.js +867 -0
  9. package/lib/typert.remote-client.d.ts +32 -0
  10. package/lib/typert.remote-client.js +210 -0
  11. package/lib/types/activity.d.ts +23 -0
  12. package/lib/types/activity.js +85 -0
  13. package/lib/types/client.d.ts +3 -0
  14. package/lib/types/client.js +3 -0
  15. package/lib/types/error.d.ts +13 -0
  16. package/lib/types/error.js +23 -0
  17. package/lib/types/index.d.ts +137 -0
  18. package/lib/types/index.js +313 -0
  19. package/lib/types/invariant.d.ts +9 -0
  20. package/lib/types/invariant.js +26 -0
  21. package/lib/types/journal.d.ts +39 -0
  22. package/lib/types/journal.js +63 -0
  23. package/lib/types/lifecycle.d.ts +33 -0
  24. package/lib/types/lifecycle.js +85 -0
  25. package/lib/types/mailbox.d.ts +78 -0
  26. package/lib/types/mailbox.js +292 -0
  27. package/lib/types/persisted.d.ts +20 -0
  28. package/lib/types/persisted.js +20 -0
  29. package/lib/types/projection.d.ts +48 -0
  30. package/lib/types/projection.js +260 -0
  31. package/lib/types/roster.d.ts +113 -0
  32. package/lib/types/roster.js +446 -0
  33. package/lib/types/session-message.d.ts +11 -0
  34. package/lib/types/session-message.js +23 -0
  35. package/lib/types/task-board.d.ts +62 -0
  36. package/lib/types/task-board.js +275 -0
  37. package/lib/types/task-graph.d.ts +21 -0
  38. package/lib/types/task-graph.js +62 -0
  39. package/lib/types/types.d.ts +206 -0
  40. package/lib/types/types.js +26 -0
  41. package/lib/types/validation.d.ts +16 -0
  42. package/lib/types/validation.js +33 -0
  43. package/package.json +88 -0
@@ -0,0 +1,113 @@
1
+ /** Team membership, continuable-child provisioning, and roster-owned teardown. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ import type { Agent } from '@deepseek-ai/dsh-agent';
4
+ import type { SessionId } from '@deepseek-ai/dsh-session';
5
+ import type { TeamJournal } from './journal.ts';
6
+ import type { TeamRuntimeLifecycle } from './lifecycle.ts';
7
+ import type { TeamState } from './projection.ts';
8
+ import { TeamId } from './types.ts';
9
+ import type { SpawnTeammateRequest, SpawnTeammateResult, TeamMemberView } from './types.ts';
10
+ /** Caller identity inside one implicit Team. */
11
+ export interface TeamMembership {
12
+ readonly root: Agent;
13
+ readonly id: TeamId;
14
+ readonly role: 'lead' | 'teammate';
15
+ readonly name: string;
16
+ }
17
+ /**
18
+ * Resolve one active Team member by model-facing name, including the Lead pseudo-row.
19
+ * @param root - exact live Team Lead.
20
+ * @param state - current Team state.
21
+ * @param rawName - candidate member name.
22
+ * @returns resolved durable id and normalized name.
23
+ */
24
+ export declare function resolveActiveMember(root: Agent, state: TeamState, rawName: string): {
25
+ id: SessionId;
26
+ name: string;
27
+ };
28
+ /** Owns Team identities and the lifecycle of rostered continuable children. */
29
+ export declare class TeamRoster {
30
+ private readonly ctx;
31
+ private readonly journal;
32
+ private readonly lifecycle;
33
+ private readonly maxMembers;
34
+ private readonly inFlightCreations;
35
+ /**
36
+ * @param ctx - Team service context with Agent, Session, persistence, and subagent services.
37
+ * @param journal - authoritative Lead-log transaction owner.
38
+ * @param lifecycle - shared Team runtime admission cutoff.
39
+ * @param maxMembers - maximum immutable roster entries per Team.
40
+ */
41
+ constructor(ctx: Context, journal: TeamJournal, lifecycle: TeamRuntimeLifecycle, maxMembers: number);
42
+ /**
43
+ * Resolve one exact live Agent's Team role.
44
+ * @param agent - exact live Agent used as the authority credential.
45
+ * @returns its root, Team identity, role, and model-facing name.
46
+ */
47
+ membership(agent: Agent): TeamMembership;
48
+ /**
49
+ * Resolve a caller without throwing for scoped installation and lifecycle observers.
50
+ * @param agent - candidate exact live Agent.
51
+ * @returns Team membership, or undefined for non-Team subagents and stale identities.
52
+ */
53
+ tryMembership(agent: Agent): TeamMembership | undefined;
54
+ /**
55
+ * List the runtime-enriched roster visible to one Team member.
56
+ * @param membership - exact caller membership resolved by this roster.
57
+ * @returns Lead and teammate rows in creation order.
58
+ */
59
+ list(membership: TeamMembership): TeamMemberView[];
60
+ /**
61
+ * Create one named, continuable direct child of the Team Lead.
62
+ * @param caller - exact live Lead Agent.
63
+ * @param request - immutable name, description, prompt, context mode, provider, and cancellation.
64
+ * @returns the active roster row.
65
+ */
66
+ spawn(caller: Agent, request: SpawnTeammateRequest): Promise<SpawnTeammateResult>;
67
+ /**
68
+ * Return admitted creation operations captured for ordered disposal.
69
+ * @returns detached snapshot ordered only by Set insertion.
70
+ */
71
+ pendingCreations(): readonly Promise<unknown>[];
72
+ /**
73
+ * Reconcile provisioning state when one Team member Session starts.
74
+ * @param agent - newly started exact live Agent.
75
+ * @param signal - shared runtime cancellation.
76
+ */
77
+ recoverFor(agent: Agent, signal: AbortSignal): Promise<void>;
78
+ /**
79
+ * Interrupt one live teammate turn without clearing its pending inbox.
80
+ * @param caller - exact live Lead Agent.
81
+ * @param targetName - durable teammate name.
82
+ * @returns the target status sampled before cancellation.
83
+ */
84
+ interrupt(caller: Agent, targetName: string): {
85
+ previousStatus: 'running' | 'idle' | 'inactive';
86
+ };
87
+ /**
88
+ * Group exact live roster children by their current Lead for runtime teardown.
89
+ * @returns each live Lead and the roster child ids currently in the Agent registry.
90
+ */
91
+ liveChildrenByRoot(): Map<Agent, SessionId[]>;
92
+ /**
93
+ * Release exact teammate Activations through the continuation lifecycle owner.
94
+ * @param root - exact live Team Lead authorizing release.
95
+ * @param childIds - selected roster child ids.
96
+ */
97
+ stopTeammates(root: Agent, childIds: readonly SessionId[]): Promise<void>;
98
+ /** Perform one creation admitted before the Team runtime disposal cutoff. */
99
+ private spawnAdmitted;
100
+ /** Flush the accepted initial inbox item before the Lead can commit `active`. */
101
+ private checkpointInitialPrompt;
102
+ /** Settle provisioning-only members from their independently durable child Sessions. */
103
+ private reconcileProvisioning;
104
+ /** Build one runtime member row after successful creation. */
105
+ private memberView;
106
+ /** Validate a never-reused model-facing teammate name. */
107
+ private memberName;
108
+ /** Append one terminal provisioning edge unless recovery already settled it. */
109
+ private settleProvisioning;
110
+ /** Whether a Session's own suffix identifies a provider-owned subagent child. */
111
+ private subagentDescriptor;
112
+ }
113
+ //# sourceMappingURL=roster.d.ts.map
@@ -0,0 +1,446 @@
1
+ /** Team membership, continuable-child provisioning, and roster-owned teardown. */
2
+ import { randomUUID } from 'node:crypto';
3
+ import { brandString } from '@deepseek-ai/dsh-brand';
4
+ import { foldSubagentDescriptor } from '@deepseek-ai/dsh-subagent';
5
+ import { errorMessage, TeamError } from "./error.js";
6
+ import { readPersistedSession } from "./persisted.js";
7
+ import { messageAccepted } from "./session-message.js";
8
+ import { TeamId } from "./types.js";
9
+ import { requiredText } from "./validation.js";
10
+ const MEMBER_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
11
+ /**
12
+ * Resolve one active Team member by model-facing name, including the Lead pseudo-row.
13
+ * @param root - exact live Team Lead.
14
+ * @param state - current Team state.
15
+ * @param rawName - candidate member name.
16
+ * @returns resolved durable id and normalized name.
17
+ */
18
+ export function resolveActiveMember(root, state, rawName) {
19
+ const name = rawName.trim();
20
+ if (name === 'lead')
21
+ return { id: root.id, name };
22
+ const member = state.members.find(candidate => candidate.name === name);
23
+ if (member === undefined || member.phase !== 'active') {
24
+ throw new TeamError(`active teammate "${name}" not found`, 'TEAM_MEMBER_NOT_FOUND');
25
+ }
26
+ return { id: member.id, name };
27
+ }
28
+ /** Owns Team identities and the lifecycle of rostered continuable children. */
29
+ export class TeamRoster {
30
+ ctx;
31
+ journal;
32
+ lifecycle;
33
+ maxMembers;
34
+ inFlightCreations = new Set();
35
+ /**
36
+ * @param ctx - Team service context with Agent, Session, persistence, and subagent services.
37
+ * @param journal - authoritative Lead-log transaction owner.
38
+ * @param lifecycle - shared Team runtime admission cutoff.
39
+ * @param maxMembers - maximum immutable roster entries per Team.
40
+ */
41
+ constructor(ctx, journal, lifecycle, maxMembers) {
42
+ this.ctx = ctx;
43
+ this.journal = journal;
44
+ this.lifecycle = lifecycle;
45
+ this.maxMembers = maxMembers;
46
+ }
47
+ /**
48
+ * Resolve one exact live Agent's Team role.
49
+ * @param agent - exact live Agent used as the authority credential.
50
+ * @returns its root, Team identity, role, and model-facing name.
51
+ */
52
+ membership(agent) {
53
+ const membership = this.tryMembership(agent);
54
+ if (membership === undefined) {
55
+ throw new TeamError(`agent "${agent.id}" is not a member of an active Agent Team`, 'TEAM_NOT_MEMBER');
56
+ }
57
+ return membership;
58
+ }
59
+ /**
60
+ * Resolve a caller without throwing for scoped installation and lifecycle observers.
61
+ * @param agent - candidate exact live Agent.
62
+ * @returns Team membership, or undefined for non-Team subagents and stale identities.
63
+ */
64
+ tryMembership(agent) {
65
+ if (this.ctx.agents.get(agent.id) !== agent)
66
+ return undefined;
67
+ try {
68
+ const parentId = agent.session.header.parentSession;
69
+ if (parentId !== undefined) {
70
+ const root = this.ctx.agents.get(parentId);
71
+ if (root !== undefined) {
72
+ const member = this.journal.state(root).members.find(candidate => candidate.id === agent.id);
73
+ if (member?.phase === 'active' || member?.phase === 'provisioning') {
74
+ return { root, id: TeamId(root.id), role: 'teammate', name: member.name };
75
+ }
76
+ // A direct child outside the durable roster is not a teammate. Ordinary
77
+ // host forks are independent roots; subagent descriptors distinguish
78
+ // provider-owned workers that must not receive a nested Team identity.
79
+ if (this.subagentDescriptor(agent))
80
+ return undefined;
81
+ return { root: agent, id: TeamId(agent.id), role: 'lead', name: 'lead' };
82
+ }
83
+ }
84
+ // A continuation can briefly outlive its parent during child-first teardown.
85
+ // Do not reinterpret that durable child as a new implicit root Team. A host-
86
+ // resumed ordinary fork has no descriptor in its own suffix and remains a
87
+ // valid new root whose inherited Team records stay outside its projected Team state.
88
+ if (this.subagentDescriptor(agent))
89
+ return undefined;
90
+ return { root: agent, id: TeamId(agent.id), role: 'lead', name: 'lead' };
91
+ }
92
+ catch {
93
+ // This method is used by lifecycle observers and teardown discovery. A
94
+ // malformed durable stream is surfaced by authoritative Team operations;
95
+ // the non-throwing probe must not veto unrelated Agent lifecycle edges.
96
+ return undefined;
97
+ }
98
+ }
99
+ /**
100
+ * List the runtime-enriched roster visible to one Team member.
101
+ * @param membership - exact caller membership resolved by this roster.
102
+ * @returns Lead and teammate rows in creation order.
103
+ */
104
+ list(membership) {
105
+ const { root } = membership;
106
+ const state = this.journal.state(root);
107
+ const result = [{
108
+ id: root.id,
109
+ name: 'lead',
110
+ role: 'lead',
111
+ status: root.status,
112
+ ...root.options.model === undefined ? {} : { model: root.options.model },
113
+ diagnostics: [],
114
+ }];
115
+ for (const member of state.members) {
116
+ const live = this.ctx.agents.get(member.id);
117
+ const model = live?.options.model ?? root.options.model;
118
+ result.push({
119
+ id: member.id,
120
+ name: member.name,
121
+ role: 'teammate',
122
+ status: member.phase === 'failed'
123
+ ? 'failed'
124
+ : member.phase === 'provisioning'
125
+ ? 'provisioning'
126
+ : live?.status ?? 'inactive',
127
+ description: member.description,
128
+ provider: member.provider,
129
+ context: member.context,
130
+ ...model === undefined ? {} : { model },
131
+ diagnostics: member.error === undefined ? [] : [member.error],
132
+ });
133
+ }
134
+ return result;
135
+ }
136
+ /**
137
+ * Create one named, continuable direct child of the Team Lead.
138
+ * @param caller - exact live Lead Agent.
139
+ * @param request - immutable name, description, prompt, context mode, provider, and cancellation.
140
+ * @returns the active roster row.
141
+ */
142
+ async spawn(caller, request) {
143
+ if (this.lifecycle.disposed)
144
+ throw new TeamError('Agent Teams service is disposing', 'TEAM_DISPOSED');
145
+ const operation = this.spawnAdmitted(caller, request);
146
+ this.inFlightCreations.add(operation);
147
+ try {
148
+ return await operation;
149
+ }
150
+ finally {
151
+ this.inFlightCreations.delete(operation);
152
+ }
153
+ }
154
+ /**
155
+ * Return admitted creation operations captured for ordered disposal.
156
+ * @returns detached snapshot ordered only by Set insertion.
157
+ */
158
+ pendingCreations() {
159
+ return [...this.inFlightCreations];
160
+ }
161
+ /**
162
+ * Reconcile provisioning state when one Team member Session starts.
163
+ * @param agent - newly started exact live Agent.
164
+ * @param signal - shared runtime cancellation.
165
+ */
166
+ async recoverFor(agent, signal) {
167
+ signal.throwIfAborted();
168
+ const membership = this.tryMembership(agent);
169
+ if (membership?.role === 'lead')
170
+ await this.reconcileProvisioning(membership.root, signal);
171
+ }
172
+ /**
173
+ * Interrupt one live teammate turn without clearing its pending inbox.
174
+ * @param caller - exact live Lead Agent.
175
+ * @param targetName - durable teammate name.
176
+ * @returns the target status sampled before cancellation.
177
+ */
178
+ interrupt(caller, targetName) {
179
+ const membership = this.membership(caller);
180
+ if (membership.role !== 'lead')
181
+ throw new TeamError('only the Team Lead can interrupt teammates', 'TEAM_LEAD_REQUIRED');
182
+ const state = this.journal.state(membership.root);
183
+ const target = resolveActiveMember(membership.root, state, targetName);
184
+ if (target.id === membership.root.id)
185
+ throw new TeamError('the Team Lead cannot interrupt itself', 'TEAM_INVALID_TARGET');
186
+ const live = this.ctx.agents.get(target.id);
187
+ if (live === undefined)
188
+ return { previousStatus: 'inactive' };
189
+ const previousStatus = live.status;
190
+ this.ctx.subagents.interrupt(target.id, { kind: 'ancestor', agent: caller });
191
+ return { previousStatus };
192
+ }
193
+ /**
194
+ * Group exact live roster children by their current Lead for runtime teardown.
195
+ * @returns each live Lead and the roster child ids currently in the Agent registry.
196
+ */
197
+ liveChildrenByRoot() {
198
+ const teams = new Map();
199
+ for (const agent of this.ctx.agents.list()) {
200
+ const rootId = agent.session.header.parentSession;
201
+ if (rootId === undefined)
202
+ continue;
203
+ const root = this.ctx.agents.get(rootId);
204
+ if (root === undefined
205
+ || !this.journal.state(root).members.some(member => member.id === agent.id))
206
+ continue;
207
+ const children = teams.get(root) ?? [];
208
+ children.push(agent.id);
209
+ teams.set(root, children);
210
+ }
211
+ return teams;
212
+ }
213
+ /**
214
+ * Release exact teammate Activations through the continuation lifecycle owner.
215
+ * @param root - exact live Team Lead authorizing release.
216
+ * @param childIds - selected roster child ids.
217
+ */
218
+ async stopTeammates(root, childIds) {
219
+ await this.lifecycle.withTimeout(this.ctx.subagents.drainContinuableChildren(root, childIds));
220
+ }
221
+ /** Perform one creation admitted before the Team runtime disposal cutoff. */
222
+ async spawnAdmitted(caller, request) {
223
+ const membership = this.membership(caller);
224
+ if (membership.role !== 'lead') {
225
+ throw new TeamError('only the Team Lead can create teammates', 'TEAM_LEAD_REQUIRED');
226
+ }
227
+ const signal = AbortSignal.any([request.signal, this.lifecycle.signal]);
228
+ signal.throwIfAborted();
229
+ const root = membership.root;
230
+ const name = this.memberName(request.name);
231
+ const description = requiredText(request.description, 'description', 200);
232
+ const childId = brandString(randomUUID());
233
+ const member = {
234
+ id: childId,
235
+ name,
236
+ description,
237
+ provider: requiredText(request.provider, 'provider', 200),
238
+ context: request.context,
239
+ phase: 'provisioning',
240
+ };
241
+ await this.journal.transact(root.id, async () => {
242
+ const state = this.journal.state(root);
243
+ if (state.members.some(member => member.name === name)) {
244
+ throw new TeamError(`teammate name "${name}" was already used in this Team`, 'TEAM_MEMBER_NAME_TAKEN');
245
+ }
246
+ if (state.members.length >= this.maxMembers) {
247
+ throw new TeamError(`Team member limit ${this.maxMembers} reached`, 'TEAM_MEMBER_LIMIT');
248
+ }
249
+ await this.journal.appendAndFlush(root, 'team/member', { version: 2, teamId: TeamId(root.id), member });
250
+ });
251
+ let started;
252
+ try {
253
+ started = await this.ctx.subagents.startContinuable({
254
+ childId,
255
+ provider: request.provider,
256
+ label: description,
257
+ request: {
258
+ prompt: request.prompt,
259
+ parent: root,
260
+ },
261
+ signal,
262
+ });
263
+ await this.checkpointInitialPrompt(childId, started.messageId, signal);
264
+ }
265
+ catch (error) {
266
+ const failed = {
267
+ ...member,
268
+ phase: 'failed',
269
+ error: errorMessage(error),
270
+ };
271
+ try {
272
+ const phase = await this.settleProvisioning(root, failed);
273
+ await this.stopTeammates(root, [childId]);
274
+ if (phase === 'active') {
275
+ throw new TeamError(`teammate "${name}" became active while its creator reported failure`, 'TEAM_PROVISIONING_CONFLICT', { cause: error });
276
+ }
277
+ }
278
+ catch (recordError) {
279
+ throw new AggregateError([error, recordError], 'teammate creation and durable failure recording both failed');
280
+ }
281
+ throw error;
282
+ }
283
+ const active = {
284
+ ...member,
285
+ phase: 'active',
286
+ };
287
+ // Once the continuation accepted its first prompt, it is a real child. If
288
+ // this checkpoint fails, keep the in-memory active edge instead of inventing
289
+ // an impossible active -> failed transition; restart reconciliation covers
290
+ // the provisioning-only durable prefix.
291
+ const settledPhase = await this.settleProvisioning(root, active);
292
+ if (settledPhase === 'failed') {
293
+ const conflict = new TeamError(`teammate "${name}" was reconciled as failed while creation was in progress`, 'TEAM_PROVISIONING_CONFLICT');
294
+ try {
295
+ await this.stopTeammates(root, [childId]);
296
+ }
297
+ catch (cleanupError) {
298
+ /* v8 ignore next -- requires the independently tested HMR settlement conflict and cleanup failure together. */
299
+ throw new AggregateError([conflict, cleanupError], 'provisioning conflict cleanup failed');
300
+ }
301
+ throw conflict;
302
+ }
303
+ return { member: this.memberView(active) };
304
+ }
305
+ /** Flush the accepted initial inbox item before the Lead can commit `active`. */
306
+ async checkpointInitialPrompt(childId, messageId, signal) {
307
+ while (true) {
308
+ signal.throwIfAborted();
309
+ const session = this.ctx.sessions.get(childId);
310
+ if (session === undefined) {
311
+ const stored = await readPersistedSession(this.ctx.sessionPersistence, childId, signal);
312
+ const suffix = stored.events.slice(stored.inheritedEventCount);
313
+ if (messageAccepted(suffix, message => message.id === messageId))
314
+ return;
315
+ throw new TeamError(`teammate "${childId}" initial prompt was not durably accepted`, 'TEAM_PROVISIONING_CONFLICT');
316
+ }
317
+ const progress = Promise.withResolvers();
318
+ // Abort can win while the durability flush is still pending; mark the
319
+ // later-awaited rejection handled without changing its eventual result.
320
+ void progress.promise.catch(() => undefined);
321
+ const stopEvent = this.ctx.on('session/event', (candidate) => {
322
+ if (candidate === session)
323
+ progress.resolve();
324
+ });
325
+ const stopDisposed = this.ctx.on('session/disposed', (candidate) => {
326
+ if (candidate === session)
327
+ progress.resolve();
328
+ });
329
+ const onAbort = () => {
330
+ const reason = signal.reason;
331
+ progress.reject(reason instanceof Error
332
+ ? reason
333
+ : new TeamError(`teammate creation aborted: ${errorMessage(reason)}`, 'TEAM_DISPOSED'));
334
+ };
335
+ signal.addEventListener('abort', onAbort, { once: true });
336
+ try {
337
+ signal.throwIfAborted();
338
+ await this.ctx.sessions.flush(session);
339
+ const suffix = session.snapshotEvents(session.inheritedEventCount);
340
+ if (messageAccepted(suffix, message => message.id === messageId))
341
+ return;
342
+ if (this.ctx.sessions.get(childId) !== session)
343
+ continue;
344
+ await progress.promise;
345
+ }
346
+ finally {
347
+ signal.removeEventListener('abort', onAbort);
348
+ stopDisposed();
349
+ stopEvent();
350
+ }
351
+ }
352
+ }
353
+ /** Settle provisioning-only members from their independently durable child Sessions. */
354
+ async reconcileProvisioning(root, signal) {
355
+ const provisioning = this.journal.state(root).members.filter(member => member.phase === 'provisioning');
356
+ for (const member of provisioning) {
357
+ signal.throwIfAborted();
358
+ // A live child means creation is still completing in this process. Its
359
+ // creator owns the terminal member edge.
360
+ if (this.ctx.agents.get(member.id) !== undefined)
361
+ continue;
362
+ let phase = 'failed';
363
+ let failure = 'provisioning did not leave a resumable child Session';
364
+ try {
365
+ const loaded = await readPersistedSession(this.ctx.sessionPersistence, member.id, signal);
366
+ const suffix = loaded.events.slice(loaded.inheritedEventCount);
367
+ const descriptor = foldSubagentDescriptor(suffix);
368
+ const acceptedInitialPrompt = messageAccepted(suffix, message => message.source.kind === 'user');
369
+ if (loaded.header.parentSession === root.id
370
+ && descriptor?.mode === 'continuable'
371
+ && descriptor.provider === member.provider
372
+ && acceptedInitialPrompt) {
373
+ phase = 'active';
374
+ }
375
+ else {
376
+ failure = 'persisted child Session does not match the provisioned continuation';
377
+ }
378
+ }
379
+ catch (error) {
380
+ failure = `child Session recovery failed: ${errorMessage(error)}`;
381
+ }
382
+ signal.throwIfAborted();
383
+ await this.journal.transact(root.id, async () => {
384
+ signal.throwIfAborted();
385
+ const current = this.journal.state(root).members.find(candidate => candidate.id === member.id);
386
+ if (current?.phase !== 'provisioning')
387
+ return;
388
+ const settled = {
389
+ ...current,
390
+ phase,
391
+ ...phase === 'failed' ? { error: failure } : {},
392
+ };
393
+ await this.journal.appendAndFlush(root, 'team/member', {
394
+ version: 2,
395
+ teamId: TeamId(root.id),
396
+ member: settled,
397
+ });
398
+ });
399
+ }
400
+ }
401
+ /** Build one runtime member row after successful creation. */
402
+ memberView(member) {
403
+ const live = this.ctx.agents.get(member.id);
404
+ return {
405
+ id: member.id,
406
+ name: member.name,
407
+ role: 'teammate',
408
+ status: live?.status ?? 'inactive',
409
+ description: member.description,
410
+ provider: member.provider,
411
+ context: member.context,
412
+ ...live?.options.model === undefined ? {} : { model: live.options.model },
413
+ diagnostics: [],
414
+ };
415
+ }
416
+ /** Validate a never-reused model-facing teammate name. */
417
+ memberName(value) {
418
+ if (!MEMBER_NAME.test(value) || value.length > 64 || value === 'lead') {
419
+ throw new TeamError('teammate name must be lower-kebab-case, at most 64 characters, and not "lead"', 'TEAM_INVALID_MEMBER_NAME');
420
+ }
421
+ return value;
422
+ }
423
+ /** Append one terminal provisioning edge unless recovery already settled it. */
424
+ async settleProvisioning(root, terminal) {
425
+ return this.journal.transact(root.id, async () => {
426
+ const current = this.journal.state(root).members.find(member => member.id === terminal.id);
427
+ /* v8 ignore next 3 -- the append-only provisioning event is committed by this operation before settlement. */
428
+ if (current === undefined) {
429
+ throw new TeamError(`provisioned teammate "${terminal.id}" disappeared`, 'TEAM_PROVISIONING_CONFLICT');
430
+ }
431
+ if (current.phase !== 'provisioning')
432
+ return current.phase;
433
+ await this.journal.appendAndFlush(root, 'team/member', {
434
+ version: 2,
435
+ teamId: TeamId(root.id),
436
+ member: terminal,
437
+ });
438
+ return terminal.phase === 'active' ? 'active' : 'failed';
439
+ });
440
+ }
441
+ /** Whether a Session's own suffix identifies a provider-owned subagent child. */
442
+ subagentDescriptor(agent) {
443
+ return foldSubagentDescriptor(agent.session.snapshotEvents(agent.session.inheritedEventCount)) !== undefined;
444
+ }
445
+ }
446
+ //# sourceMappingURL=roster.js.map
@@ -0,0 +1,11 @@
1
+ /** Durable Session-message acceptance checks shared by provisioning and mailbox recovery. */
2
+ import type { UserMessage } from '@deepseek-ai/dsh-llm';
3
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
4
+ /**
5
+ * Test whether one message is model-visible or still durably pending.
6
+ * @param events - one Session's non-inherited event suffix.
7
+ * @param predicate - identity check for the accepted message.
8
+ * @returns whether history or the current inbox contains a match.
9
+ */
10
+ export declare function messageAccepted(events: readonly SessionEvent[], predicate: (message: UserMessage) => boolean): boolean;
11
+ //# sourceMappingURL=session-message.d.ts.map
@@ -0,0 +1,23 @@
1
+ /** Durable Session-message acceptance checks shared by provisioning and mailbox recovery. */
2
+ /** Fold the durable inbox suffix into the messages still awaiting a claim. */
3
+ function pendingInboxMessages(events) {
4
+ const inbox = { 'next-turn': [], 'next-step': [] };
5
+ for (const event of events) {
6
+ if (event.type !== 'agent/inbox/spliced')
7
+ continue;
8
+ const pending = inbox[event.data.target];
9
+ pending.splice(event.data.start, event.data.removedCount ?? 0, ...event.data.inserted);
10
+ }
11
+ return [...inbox['next-turn'], ...inbox['next-step']];
12
+ }
13
+ /**
14
+ * Test whether one message is model-visible or still durably pending.
15
+ * @param events - one Session's non-inherited event suffix.
16
+ * @param predicate - identity check for the accepted message.
17
+ * @returns whether history or the current inbox contains a match.
18
+ */
19
+ export function messageAccepted(events, predicate) {
20
+ return events.some(event => event.type === 'user/message' && predicate(event.data))
21
+ || pendingInboxMessages(events).some(predicate);
22
+ }
23
+ //# sourceMappingURL=session-message.js.map
@@ -0,0 +1,62 @@
1
+ /** Shared Team task DAG commands and runtime-enriched views. */
2
+ import type { Agent } from '@deepseek-ai/dsh-agent';
3
+ import type { TeamMembership } from './roster.ts';
4
+ import type { TeamJournal } from './journal.ts';
5
+ import { TeamTaskId } from './types.ts';
6
+ import type { CreateTeamTaskRequest, TeamTaskView, UpdateTeamTaskRequest } from './types.ts';
7
+ /** Owns Team task limits, authorization, transitions, and derived views. */
8
+ export declare class TeamTaskBoard {
9
+ private readonly journal;
10
+ private readonly maxTasks;
11
+ /**
12
+ * @param journal - authoritative Lead-log transaction owner.
13
+ * @param maxTasks - maximum non-deleted tasks retained by one Team.
14
+ */
15
+ constructor(journal: TeamJournal, maxTasks: number);
16
+ /**
17
+ * Create one unowned pending task in the Team Lead log.
18
+ * @param membership - exact caller membership resolved by the Team roster.
19
+ * @param request - task text, blockers, and advisory write scopes.
20
+ * @returns the revision-one task view.
21
+ */
22
+ create(membership: TeamMembership, request: CreateTeamTaskRequest): Promise<TeamTaskView>;
23
+ /**
24
+ * Return one task, including a deleted tombstone.
25
+ * @param membership - exact caller membership resolved by the Team roster.
26
+ * @param id - Team-local task identity.
27
+ * @returns the latest task value and derived readiness diagnostics.
28
+ */
29
+ get(membership: TeamMembership, id: TeamTaskId): TeamTaskView;
30
+ /**
31
+ * List current non-deleted tasks in numeric creation order.
32
+ * @param membership - exact caller membership resolved by the Team roster.
33
+ * @returns detached current task views.
34
+ */
35
+ list(membership: TeamMembership): TeamTaskView[];
36
+ /**
37
+ * Compare-and-set one authorized task transition.
38
+ * @param caller - exact live Team member authorizing the mutation.
39
+ * @param membership - caller role and exact live Lead.
40
+ * @param request - task identity, expected revision, action, and action fields.
41
+ * @returns the committed next task revision.
42
+ */
43
+ update(caller: Agent, membership: TeamMembership, request: UpdateTeamTaskRequest): Promise<TeamTaskView>;
44
+ /** Validate and de-duplicate dependency ids against the current task graph. */
45
+ private dependencies;
46
+ /** Normalize and de-duplicate task write scopes. */
47
+ private writeScopes;
48
+ /** Map shared task-graph validation onto stable command error codes. */
49
+ private assertTaskGraph;
50
+ /** Whether all current blockers completed. */
51
+ private taskReady;
52
+ /** Remove an optional owner field under exactOptionalPropertyTypes. */
53
+ private withoutOwner;
54
+ /**
55
+ * Build one task view with owner name, readiness, and advisory write overlaps.
56
+ * A committing caller may pass its pre-append state because `task` supplies the
57
+ * new value explicitly; owner names, blocker readiness, and other task scopes
58
+ * do not change when that snapshot is appended.
59
+ */
60
+ private taskView;
61
+ }
62
+ //# sourceMappingURL=task-board.d.ts.map