@fishwithcat/dsh-subagent 0.0.0-stage → 0.2.2-alpha.3

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 (59) hide show
  1. package/README.i18n.yaml +121 -0
  2. package/README.md +258 -2
  3. package/README.zh.md +259 -0
  4. package/lib/index.js +3065 -0
  5. package/lib/typert.host.d.ts +3 -0
  6. package/lib/typert.host.js +1086 -0
  7. package/lib/typert.remote-client.d.ts +29 -0
  8. package/lib/typert.remote-client.js +122 -0
  9. package/lib/types/activation.d.ts +110 -0
  10. package/lib/types/activation.js +112 -0
  11. package/lib/types/archive-admission.d.ts +23 -0
  12. package/lib/types/archive-admission.js +75 -0
  13. package/lib/types/assistant-output.d.ts +48 -0
  14. package/lib/types/assistant-output.js +73 -0
  15. package/lib/types/catalog.d.ts +87 -0
  16. package/lib/types/catalog.js +113 -0
  17. package/lib/types/child-agent.d.ts +145 -0
  18. package/lib/types/child-agent.js +229 -0
  19. package/lib/types/client.d.ts +8 -0
  20. package/lib/types/client.js +7 -0
  21. package/lib/types/continuation-messages.d.ts +66 -0
  22. package/lib/types/continuation-messages.js +116 -0
  23. package/lib/types/control-types.d.ts +121 -0
  24. package/lib/types/control-types.js +8 -0
  25. package/lib/types/control.d.ts +41 -0
  26. package/lib/types/control.js +75 -0
  27. package/lib/types/depth.d.ts +31 -0
  28. package/lib/types/depth.js +39 -0
  29. package/lib/types/descriptor.d.ts +105 -0
  30. package/lib/types/descriptor.js +192 -0
  31. package/lib/types/error.d.ts +11 -0
  32. package/lib/types/error.js +14 -0
  33. package/lib/types/index.d.ts +282 -0
  34. package/lib/types/index.js +398 -0
  35. package/lib/types/lifecycle.d.ts +54 -0
  36. package/lib/types/lifecycle.js +89 -0
  37. package/lib/types/list-children.d.ts +36 -0
  38. package/lib/types/list-children.js +166 -0
  39. package/lib/types/manager.d.ts +215 -0
  40. package/lib/types/manager.js +1171 -0
  41. package/lib/types/out-of-process.d.ts +92 -0
  42. package/lib/types/out-of-process.js +177 -0
  43. package/lib/types/projection-types.d.ts +93 -0
  44. package/lib/types/projection-types.js +7 -0
  45. package/lib/types/projection.d.ts +94 -0
  46. package/lib/types/projection.js +157 -0
  47. package/lib/types/structured.d.ts +49 -0
  48. package/lib/types/structured.js +130 -0
  49. package/lib/types/types.d.ts +331 -0
  50. package/lib/types/types.js +17 -0
  51. package/locale/de.json +6 -0
  52. package/locale/en.json +5 -0
  53. package/locale/es.json +6 -0
  54. package/locale/fr.json +6 -0
  55. package/locale/ja.json +6 -0
  56. package/locale/pt.json +6 -0
  57. package/locale/ru.json +6 -0
  58. package/locale/zh.json +6 -0
  59. package/package.json +131 -4
@@ -0,0 +1,1171 @@
1
+ /**
2
+ * Owns subagent startup, message admission, cold resume, and execution lifetimes.
3
+ * One child lock orders input acceptance and closure; one activation map owns
4
+ * capacity, runtime ownership, and resource release across all providers.
5
+ *
6
+ * @module @deepseek-ai/dsh-subagent/manager
7
+ */
8
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
9
+ if (value !== null && value !== void 0) {
10
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
11
+ var dispose, inner;
12
+ if (async) {
13
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
14
+ dispose = value[Symbol.asyncDispose];
15
+ }
16
+ if (dispose === void 0) {
17
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
18
+ dispose = value[Symbol.dispose];
19
+ if (async) inner = dispose;
20
+ }
21
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
22
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
23
+ env.stack.push({ value: value, dispose: dispose, async: async });
24
+ }
25
+ else if (async) {
26
+ env.stack.push({ async: true });
27
+ }
28
+ return value;
29
+ };
30
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
31
+ return function (env) {
32
+ function fail(e) {
33
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
34
+ env.hasError = true;
35
+ }
36
+ var r, s = 0;
37
+ function next() {
38
+ while (r = env.stack.pop()) {
39
+ try {
40
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
41
+ if (r.dispose) {
42
+ var result = r.dispose.call(r.value);
43
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
44
+ }
45
+ else s |= 1;
46
+ }
47
+ catch (e) {
48
+ fail(e);
49
+ }
50
+ }
51
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
52
+ if (env.hasError) throw env.error;
53
+ }
54
+ return next();
55
+ };
56
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
57
+ var e = new Error(message);
58
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
59
+ });
60
+ import { randomUUID } from 'node:crypto';
61
+ import { isAbsolute, resolve } from 'node:path';
62
+ import { brandString } from '@deepseek-ai/dsh-brand';
63
+ import { ReasoningEffortId, contentHasImage, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm';
64
+ import { SessionLogOffset } from '@deepseek-ai/dsh-session';
65
+ import { appendDelegatedPolicyOverrides, applyChildComposition, captureDelegatedPolicyOverrides, childSessionMeta, resolveChildAgentOptions, resolveChildDepth, } from "./child-agent.js";
66
+ import { createAgentMessage, createSettlementMessage, withResultDeliveryGuidance, } from "./continuation-messages.js";
67
+ import { installSubagentArchiveAdmission } from "./archive-admission.js";
68
+ import { foldSubagentDescriptor, snapshotSubagentDescriptor } from "./descriptor.js";
69
+ import { establishCatalogChild, establishExternalCatalogChild } from "./catalog.js";
70
+ import { SubagentError } from "./error.js";
71
+ import { SubagentRunId } from "./types.js";
72
+ import { attachStructuredRuntime } from "./structured.js";
73
+ import { ActivationPool, ChildLock, activationAgent, captureLocalResult, observeLocalFailure, requireLocalActivation } from "./activation.js";
74
+ /** Render only the safe outer message from execution or teardown failures. */
75
+ function failureMessage(error) {
76
+ return error instanceof Error ? error.message : 'unknown teardown failure';
77
+ }
78
+ /** Own subagent requests and the resources of every live activation. */
79
+ export class SubagentManager {
80
+ ctx;
81
+ host;
82
+ maxActiveSubagents;
83
+ delegatedApproval;
84
+ /** Child session id → its live Activation. Process-local, never durable. */
85
+ resident = new Map();
86
+ /** Root identities retain their pool across child settlement without retaining dead roots. */
87
+ rootPools = new WeakMap();
88
+ /** Materializations admitted before drain, tracked through publication or rollback. */
89
+ materializations = new Set();
90
+ /** Per-child serializer shared by delivery, release, and disposal. */
91
+ locks = new ChildLock();
92
+ /** Structural Cordis owner of every Activation handle. */
93
+ ownerCtx;
94
+ /** Exact subagent tree roots whose host teardown has closed admission. */
95
+ closingRoots = new Set();
96
+ draining = false;
97
+ /**
98
+ * Build one manager inside the service's Agent-injected context.
99
+ * @param ctx - context providing Agents, Sessions, and teardown ownership.
100
+ * @param host - descriptor reads and lifecycle publication owned by the service.
101
+ * @param maxActiveSubagents - the current configured child capacity.
102
+ * @param delegatedApproval - the configured approval policy seed for each local child.
103
+ */
104
+ constructor(ctx, host, maxActiveSubagents, delegatedApproval) {
105
+ this.ctx = ctx;
106
+ this.host = host;
107
+ this.maxActiveSubagents = maxActiveSubagents;
108
+ this.delegatedApproval = delegatedApproval;
109
+ // Drain managed instances before Cordis releases their owning scope.
110
+ const scope = ctx.plugin(function activationOwner() { });
111
+ this.ownerCtx = scope.ctx;
112
+ installSubagentArchiveAdmission(ctx, (agent) => {
113
+ const activation = this.resident.get(agent.id);
114
+ return activation !== undefined && activationAgent(activation) === agent ? activation.ancestors : undefined;
115
+ }, session => host.identityOf(session));
116
+ ctx.on('agent/disposed', ({ agent }) => {
117
+ this.closingRoots.delete(agent);
118
+ });
119
+ ctx.effect(function* () {
120
+ yield scope.dispose;
121
+ yield () => this.drain();
122
+ }.bind(this), 'subagents.manager()');
123
+ }
124
+ /**
125
+ * Interrupt one live continuable child's current turn under the supplied authority.
126
+ * @param targetSessionId - the durable child session id to interrupt.
127
+ * @param authority - a trusted user operation or exact live ancestor Agent.
128
+ */
129
+ interrupt(targetSessionId, authority) {
130
+ if (authority.kind === 'ancestor') {
131
+ const caller = authority.agent;
132
+ if (this.ctx.agents.get(caller.id) !== caller) {
133
+ throw new SubagentError(`interrupting "${targetSessionId}" requires the exact live ancestor agent`, 'UNAUTHORIZED');
134
+ }
135
+ if (caller.id === targetSessionId) {
136
+ throw new SubagentError(`agent "${caller.id}" cannot interrupt itself`, 'UNAUTHORIZED');
137
+ }
138
+ }
139
+ const activation = this.resident.get(targetSessionId);
140
+ if (activation === undefined)
141
+ return;
142
+ if (authority.kind === 'ancestor'
143
+ && (this.rootOf(authority.agent) !== activation.root || !activation.ancestors.includes(authority.agent.id))) {
144
+ throw new SubagentError(`subagent "${targetSessionId}" is not a live descendant of agent "${authority.agent.id}"`, 'UNAUTHORIZED');
145
+ }
146
+ // Disposal already stopped the target with a whole-Activation teardown;
147
+ // a second cancel would be a redundant signal on a closing handle.
148
+ if (activation.closing !== undefined)
149
+ return;
150
+ const kind = authority.kind === 'user' ? 'user' : 'parent';
151
+ if (activation.kind === 'local') {
152
+ activation.handle.agent.cancel({ kind }, { keepInbox: true });
153
+ }
154
+ else {
155
+ activation.controller.abort({ kind });
156
+ // The settlement observer reports teardown failures.
157
+ void this.dispose(activation).catch(() => undefined);
158
+ }
159
+ }
160
+ /**
161
+ * Close admission, await every already-admitted materialization through
162
+ * publication or rollback, then dispose every agent in the subagent tree.
163
+ */
164
+ async drain() {
165
+ this.draining = true;
166
+ for (const pending of this.materializations)
167
+ pending.controller.abort();
168
+ await Promise.all([...this.materializations].map(materialization => materialization.settled));
169
+ await this.disposeRoots([...this.resident.values()], 'activation(s)');
170
+ }
171
+ /**
172
+ * Join progressing subagent tree work for its root, leaving idle parked inboxes resident.
173
+ * @param root - exact live subagent tree root; ordinary members return false.
174
+ * @returns whether work was joined and the host must recheck root completion.
175
+ */
176
+ async waitForTreeWork(root) {
177
+ if (this.rootOf(root) !== root)
178
+ return false;
179
+ let observed = false;
180
+ while (true) {
181
+ const pending = [...this.materializations].filter(item => item.root === root);
182
+ const children = [...this.resident.values()].filter(item => item.root === root);
183
+ observed ||= children.some(item => item.kind === 'local' && item.handle.agent.status !== 'idle');
184
+ await Promise.all(children.flatMap(item => item.kind === 'local' ? [item.handle.agent.whenIdle()] : []));
185
+ const waits = pending.map(item => item.settled);
186
+ for (const item of children) {
187
+ if (item.kind === 'external' || item.closing !== undefined) {
188
+ waits.push(item.result.promise.catch(() => undefined));
189
+ continue;
190
+ }
191
+ if (item.handle.agent.status !== 'idle') {
192
+ waits.push(item.handle.agent.whenIdle());
193
+ continue;
194
+ }
195
+ const inbox = item.handle.agent.inbox;
196
+ if (item.failureAt === undefined && (inbox.nextTurn.length > 0 || inbox.nextStep.length > 0))
197
+ continue;
198
+ waits.push(Promise.race([item.result.promise.catch(() => undefined), item.poke.promise]));
199
+ }
200
+ if (waits.length > 0) {
201
+ observed = true;
202
+ await Promise.all(waits);
203
+ continue;
204
+ }
205
+ // A descendant can be published while whenIdle follows another child.
206
+ if ([...this.materializations].some(item => item.root === root && !pending.includes(item))
207
+ || [...this.resident.values()].some(item => item.root === root && !children.includes(item)))
208
+ continue;
209
+ return observed;
210
+ }
211
+ }
212
+ /**
213
+ * Stop every member of the selected subagent trees; ordinary members do not close a subagent tree.
214
+ * @param parents - exact live subagent tree roots whose instances must stop.
215
+ */
216
+ async drainDescendants(parents) {
217
+ const roots = new Set(parents.filter(root => this.ctx.agents.get(root.id) === root && this.rootOf(root) === root));
218
+ for (const root of roots)
219
+ this.closingRoots.add(root);
220
+ const pending = [...this.materializations].filter(item => roots.has(item.root));
221
+ for (const item of pending)
222
+ item.controller.abort();
223
+ const targets = [...this.resident.values()].filter(item => roots.has(item.root));
224
+ const disposal = this.disposeRoots(targets, 'scoped activation(s)');
225
+ const [disposed] = await Promise.allSettled([disposal, ...pending.map(item => item.settled)]);
226
+ if (disposed.status === 'rejected')
227
+ throw disposed.reason;
228
+ }
229
+ /**
230
+ * Start one continuable background child and resolve at initial inbox acceptance.
231
+ * Every earlier failure disposes any created handle and rolls back Activation
232
+ * and its capacity slot without returning either id.
233
+ * @param spec - provider, delegation request, and caller cancellation.
234
+ * @param prepare - creation contribution bound to the checked provider.
235
+ * @returns the durable child id and accepted initial prompt message id.
236
+ */
237
+ async startLocal(spec, prepare) {
238
+ const parent = spec.parent;
239
+ this.assertAdmitting(parent);
240
+ const ancestry = this.ancestryForChild(parent);
241
+ return this.trackStartup(ancestry.root, spec.signal, async (signal) => {
242
+ const persistence = spec.delivery === 'caller' ? this.ctx.get('sessionPersistence') : this.requirePersistence();
243
+ const childId = spec.childId ?? brandString(randomUUID());
244
+ this.assertChildIdAvailable(childId);
245
+ const childDepth = resolveChildDepth(parent, spec.maxDepth);
246
+ // Snapshot before any await: invalid descriptor JSON rejects the call
247
+ // before a child exists, and the detached value is what reaches the log.
248
+ const agentOptions = resolveChildAgentOptions(parent, spec.agentOptions, childDepth);
249
+ const agentProvider = agentOptions.provider;
250
+ const agentModel = agentOptions.model;
251
+ const agentReasoningEffort = agentOptions.reasoningEffort;
252
+ const descriptor = snapshotSubagentDescriptor({
253
+ mode: 'continuable',
254
+ provider: spec.provider,
255
+ label: spec.label,
256
+ ...agentProvider !== undefined ? { agentProvider } : {},
257
+ ...agentModel !== undefined ? { agentModel } : {},
258
+ ...agentReasoningEffort !== undefined ? { agentReasoningEffort } : {},
259
+ });
260
+ // Capture before the first await: a later parent switch belongs to the
261
+ // parent's future, not to this child.
262
+ const delegatedPolicies = captureDelegatedPolicyOverrides(parent, this.delegatedApproval());
263
+ const cwd = await this.resolveDirectory(spec, signal);
264
+ const prepared = await prepare({
265
+ sessionId: childId,
266
+ cwd,
267
+ parent,
268
+ signal,
269
+ });
270
+ signal.throwIfAborted();
271
+ this.assertAdmitting(parent);
272
+ const inheritedEventCount = SessionLogOffset(prepared.seed?.length ?? 0);
273
+ const seed = prepared.seed;
274
+ let established;
275
+ const messageId = await this.locks.run(childId, async () => {
276
+ signal.throwIfAborted();
277
+ this.assertAdmitting(parent);
278
+ this.assertChildIdAvailable(childId);
279
+ if (spec.childId !== undefined) {
280
+ const persisted = await persistence?.stat(childId, { signal: signal });
281
+ signal.throwIfAborted();
282
+ this.assertAdmitting(parent);
283
+ this.assertChildIdAvailable(childId);
284
+ if (persisted !== undefined) {
285
+ throw new SubagentError(`subagent "${childId}" already exists`, 'DUPLICATE_CHILD');
286
+ }
287
+ }
288
+ const activation = await this.materialize({
289
+ kind: 'local',
290
+ childId,
291
+ provider: spec.provider,
292
+ ...ancestry,
293
+ create: {
294
+ parent,
295
+ cwd,
296
+ seed,
297
+ meta: childSessionMeta(parent, childDepth, prepared.seed !== undefined, spec.agentPreset),
298
+ inheritedEventCount,
299
+ delegatedPolicies,
300
+ descriptor,
301
+ },
302
+ agentOptions,
303
+ composition: { agentPreset: spec.agentPreset },
304
+ signal,
305
+ delivery: spec.delivery,
306
+ outputSchema: spec.outputSchema,
307
+ });
308
+ established = activation;
309
+ const child = requireLocalActivation(activation).handle.agent;
310
+ const childHeader = child.session.header;
311
+ return await this.submitMaterialized(activation, spec.delivery !== 'caller'
312
+ ? withResultDeliveryGuidance(spec.prompt)
313
+ : spec.prompt, { source: { kind: 'user' }, signal, delivery: 'queue', authorize: () => { this.assertAdmitting(parent); } }, ancestry.root, () => {
314
+ this.assertAdmitting(parent);
315
+ establishCatalogChild(parent.session, childHeader, descriptor);
316
+ });
317
+ });
318
+ return { ...this.receipt(established), childId, messageId };
319
+ });
320
+ }
321
+ /**
322
+ * Start an external backend under activation ownership.
323
+ * @param spec - task, provider, cancellation, and delivery policy.
324
+ * @param start - execution bound to the checked provider.
325
+ * @returns the accepted activation and its result/disposal capabilities.
326
+ */
327
+ async startExternal(spec, start) {
328
+ const { provider, delivery, childId: _childId, ...request } = spec;
329
+ const parent = request.parent;
330
+ this.assertAdmitting(parent);
331
+ const ancestry = this.ancestryForChild(parent);
332
+ return this.trackStartup(ancestry.root, spec.signal, async (signal) => {
333
+ let activation;
334
+ try {
335
+ const cwd = await this.resolveDirectory(spec, signal);
336
+ activation = await this.materialize({
337
+ kind: 'external',
338
+ provider,
339
+ ...ancestry,
340
+ signal,
341
+ delivery,
342
+ start: signal => start({
343
+ ...request, cwd, signal,
344
+ }),
345
+ });
346
+ this.assertAdmitting(parent);
347
+ signal.throwIfAborted();
348
+ if (delivery === 'parent') {
349
+ establishExternalCatalogChild(parent.session, activation.childId, spec.label);
350
+ }
351
+ this.accept(activation);
352
+ return this.receipt(activation);
353
+ }
354
+ catch (error) {
355
+ if (activation !== undefined) {
356
+ try {
357
+ await this.dispose(activation);
358
+ }
359
+ catch (cleanupError) {
360
+ this.ctx.logger.warn(`subagent "${activation.childId}" admission rollback failed: ${failureMessage(cleanupError)}`);
361
+ }
362
+ }
363
+ throw error;
364
+ }
365
+ });
366
+ }
367
+ /** Capture the explicit or inherited directory while the parent still owns startup. */
368
+ async resolveDirectory(request, signal) {
369
+ const cwd = request.cwd !== undefined && isAbsolute(request.cwd)
370
+ ? request.cwd
371
+ : resolve(await this.ctx.workingDirectory.ensure(request.parent, signal), request.cwd ?? '.');
372
+ signal.throwIfAborted();
373
+ return cwd;
374
+ }
375
+ /**
376
+ * Resolve the live subagent tree root from runtime ownership, independent of creator residency.
377
+ * @param caller - exact live Agent selecting the subagent tree.
378
+ * @returns the live subagent tree root.
379
+ * @throws when the caller has no active runtime owner.
380
+ */
381
+ rootOf(caller) {
382
+ if (this.ctx.agents.get(caller.id) !== caller) {
383
+ throw new SubagentError('subagent tree access requires the exact live caller', 'UNAUTHORIZED');
384
+ }
385
+ const identity = this.host.identityOf(caller.session);
386
+ const delegated = caller.session.header.origin === 'subagent' || identity != null && caller.session.isOwnSeq(identity.seq);
387
+ const activation = this.resident.get(caller.id);
388
+ if (activation !== undefined && activationAgent(activation) === caller
389
+ && this.ctx.agents.get(activation.root.id) === activation.root)
390
+ return activation.root;
391
+ if (delegated || activation !== undefined) {
392
+ throw new SubagentError('agent has no active runtime owner', 'UNAUTHORIZED');
393
+ }
394
+ return caller;
395
+ }
396
+ /** Capture creation ancestry separately from the root that owns the new instance. */
397
+ ancestryForChild(creator) {
398
+ const root = this.rootOf(creator);
399
+ const ancestors = this.resident.get(creator.id)?.ancestors ?? [];
400
+ return { root, ancestors: [...ancestors, creator.id] };
401
+ }
402
+ /**
403
+ * Deliver to another agent in the subagent tree, restoring only the target when absent.
404
+ * @param sender - exact live root or resident continuable Agent.
405
+ * @param targetId - another member's durable Session id.
406
+ * @param content - model-authored message blocks.
407
+ * @param options - cancellation before inbox acceptance.
408
+ * @returns the accepted inbox message id.
409
+ */
410
+ async sendMessage(sender, targetId, content, options) {
411
+ options.signal.throwIfAborted();
412
+ const root = this.rootOf(sender);
413
+ if (sender.id === targetId)
414
+ throw new SubagentError('send_message requires another agent in the subagent tree', 'UNAUTHORIZED');
415
+ this.assertAdmitting(sender);
416
+ const message = createAgentMessage(sender, content);
417
+ const activation = this.resident.get(sender.id);
418
+ return this.deliverToMember(root, targetId, [...message.content], {
419
+ signal: options.signal, delivery: 'steer', source: message.source,
420
+ authorize: () => { this.assertAdmitting(sender); },
421
+ onAccepted: () => {
422
+ if (activation?.kind === 'local' && activation.ancestors.at(-1) === targetId) {
423
+ activation.omitClosingText = true;
424
+ }
425
+ },
426
+ });
427
+ }
428
+ /** Resolve the live owner through verified creation edges without activating ancestors. */
429
+ async resolveCreationAncestry(source, signal, root) {
430
+ const ancestors = [];
431
+ const seen = new Set([source.header.id]);
432
+ let { header, projections, inheritedEventCount } = source;
433
+ while (true) {
434
+ const env_1 = { stack: [], error: void 0, hasError: false };
435
+ try {
436
+ signal.throwIfAborted();
437
+ const parentId = header.parentSession;
438
+ const identity = projections?.values.subagent;
439
+ if (parentId === undefined || identity == null || identity.seq < inheritedEventCount)
440
+ break;
441
+ if (seen.has(parentId))
442
+ throw new SubagentError('cyclic subagent creation ancestry', 'UNAUTHORIZED');
443
+ seen.add(parentId);
444
+ const parent = __addDisposableResource(env_1, await this.observeContinuation(parentId, signal), false);
445
+ if (!parent.projections?.values.subagentCatalog?.some(entry => entry.id === header.id)) {
446
+ throw new SubagentError('subagent is absent from its creator catalog', 'UNAUTHORIZED');
447
+ }
448
+ ancestors.unshift(parentId);
449
+ if (parentId === root?.id)
450
+ return { root, ancestors };
451
+ ({ header, projections, inheritedEventCount } = parent);
452
+ }
453
+ catch (e_1) {
454
+ env_1.error = e_1;
455
+ env_1.hasError = true;
456
+ }
457
+ finally {
458
+ __disposeResources(env_1);
459
+ }
460
+ }
461
+ if (root === undefined && ancestors.length > 0 && header.origin !== 'subagent') {
462
+ const owner = this.ctx.agents.get(header.id);
463
+ if (owner === undefined || this.rootOf(owner) !== owner) {
464
+ throw new SubagentError('subagent tree root is not live', 'CONTINUATION_UNAVAILABLE');
465
+ }
466
+ return { root: owner, ancestors };
467
+ }
468
+ throw new SubagentError(`agent "${source.header.id}" is not a member of this subagent tree`, 'UNAUTHORIZED');
469
+ }
470
+ /**
471
+ * Deliver one host-authored prompt through the target's live subagent tree root.
472
+ * Remote callers must be authorized for the target Session before invoking this operation.
473
+ * @param childId - durable continuable subagent session id.
474
+ * @param content - model-visible prompt blocks.
475
+ * @param options - inbox destination, durable attribution, and cancellation before acceptance.
476
+ * @returns the accepted durable message id.
477
+ */
478
+ async deliverPrompt(childId, content, options) {
479
+ options.signal.throwIfAborted();
480
+ let root = this.resident.get(childId)?.root;
481
+ if (root === undefined) {
482
+ const env_2 = { stack: [], error: void 0, hasError: false };
483
+ try {
484
+ const source = __addDisposableResource(env_2, await this.observeContinuation(childId, options.signal), false);
485
+ ({ root } = await this.resolveCreationAncestry(source, options.signal));
486
+ }
487
+ catch (e_2) {
488
+ env_2.error = e_2;
489
+ env_2.hasError = true;
490
+ }
491
+ finally {
492
+ __disposeResources(env_2);
493
+ }
494
+ }
495
+ return this.deliverToMember(root, childId, content, options);
496
+ }
497
+ /** Admit local input synchronously before any teardown can begin. */
498
+ deliver(activation, message, delivery) {
499
+ if (activation.closing !== undefined) {
500
+ throw new SubagentError('subagent activation is being disposed; the message was not accepted', 'ACTIVATION_CLOSING');
501
+ }
502
+ const local = requireLocalActivation(activation);
503
+ if (local.structured?.acceptsInput() === false) {
504
+ throw new SubagentError('subagent is submitting or has submitted its structured result; the message was not accepted', 'INPUT_CLOSED');
505
+ }
506
+ if (delivery === 'steer')
507
+ local.handle.agent.steer(message);
508
+ else
509
+ local.handle.agent.followup(message);
510
+ }
511
+ /**
512
+ * Reject one child identity already owned by a live Agent or Session.
513
+ * @param childId - proposed durable child session id.
514
+ */
515
+ assertChildIdAvailable(childId) {
516
+ if (this.resident.has(childId) || this.ctx.agents.get(childId) !== undefined || this.ctx.get('sessions')?.get(childId) !== undefined) {
517
+ throw new SubagentError(`subagent "${childId}" already exists`, 'DUPLICATE_CHILD');
518
+ }
519
+ }
520
+ /**
521
+ * Apply activation admission to a resident parent before delivering input.
522
+ * @param parent - exact live Agent receiving the message.
523
+ * @param message - durable user message to deliver.
524
+ * @param delivery - receiving inbox destination.
525
+ */
526
+ sendWaking(parent, message, delivery) {
527
+ const parentActivation = this.resident.get(parent.id);
528
+ if (parentActivation !== undefined && activationAgent(parentActivation) === parent) {
529
+ try {
530
+ this.deliver(parentActivation, message, delivery);
531
+ }
532
+ finally {
533
+ this.wake(parentActivation);
534
+ }
535
+ return;
536
+ }
537
+ if (delivery === 'steer')
538
+ parent.steer(message);
539
+ else
540
+ parent.followup(message);
541
+ }
542
+ /**
543
+ * Reject new work from closing members or subagent trees.
544
+ * @param agent - exact live caller whose instance and subagent tree must admit work.
545
+ */
546
+ assertAdmitting(agent) {
547
+ const closing = this.closingTeardownFor(agent);
548
+ if (closing === undefined) {
549
+ if (this.resident.get(agent.id)?.closing !== undefined) {
550
+ throw new SubagentError(`subagent "${agent.id}" is closing`, 'ACTIVATION_CLOSING');
551
+ }
552
+ return;
553
+ }
554
+ throw new SubagentError(closing === 'manager'
555
+ ? 'subagents are draining; the operation was not admitted'
556
+ : `subagent tree "${closing.id}" is draining; the operation was not admitted`, 'DRAINING');
557
+ }
558
+ /**
559
+ * Create or resume one child Agent and publish its Activation.
560
+ * @param inputs - reconstruction and admission inputs for the residency epoch.
561
+ * @returns the published process-local Activation.
562
+ */
563
+ materialize(inputs) {
564
+ this.assertAdmitting(inputs.root);
565
+ inputs.signal.throwIfAborted();
566
+ const pool = this.rootPool(inputs.root);
567
+ const releaseSlot = pool.reserve(this.maxActiveSubagents());
568
+ return this.materializeTracked(inputs, releaseSlot).catch((error) => {
569
+ releaseSlot();
570
+ throw error;
571
+ });
572
+ }
573
+ /** Keep subagent tree shutdown and host waits joined to preparation, publication, and rollback. */
574
+ trackStartup(root, signal, start) {
575
+ this.assertAdmitting(root);
576
+ signal.throwIfAborted();
577
+ const settled = Promise.withResolvers();
578
+ const controller = new AbortController();
579
+ const materialization = { root, settled: settled.promise, controller };
580
+ this.materializations.add(materialization);
581
+ return start(AbortSignal.any([signal, controller.signal])).catch((error) => {
582
+ if (error instanceof Error && error.name === 'AbortError')
583
+ this.assertAdmitting(root);
584
+ throw error;
585
+ }).finally(() => {
586
+ this.materializations.delete(materialization);
587
+ settled.resolve();
588
+ });
589
+ }
590
+ /**
591
+ * Stop and release one Activation through its memoized close transaction.
592
+ * @param activation - exact residency epoch to close.
593
+ * @returns the shared close transaction.
594
+ */
595
+ dispose(activation) {
596
+ return this.close(activation, () => this.finishDisposal(activation, true));
597
+ }
598
+ /** Close admission synchronously and share one release, including startup rollback. */
599
+ close(activation, release) {
600
+ if (activation.closing !== undefined)
601
+ return activation.closing;
602
+ const completion = Promise.withResolvers();
603
+ activation.closing = completion.promise;
604
+ void release().then(completion.resolve, completion.reject);
605
+ return completion.promise;
606
+ }
607
+ /** Dispose independent roots and report every branch failure after all settle. */
608
+ async disposeRoots(roots, failureSubject) {
609
+ const failures = await Promise.all(roots.map(async (activation) => {
610
+ try {
611
+ await this.dispose(activation);
612
+ return undefined;
613
+ }
614
+ catch (error) {
615
+ return error;
616
+ }
617
+ }));
618
+ const reasons = failures.filter(failure => failure !== undefined);
619
+ if (reasons.length > 0) {
620
+ throw new SubagentError(`subagent teardown failed for ${reasons.length} ${failureSubject}: `
621
+ + reasons.map(reason => failureMessage(reason)).join('; '), 'ACTIVATION_TEARDOWN_FAILED');
622
+ }
623
+ }
624
+ /** Return the subagent tree teardown that closed admission for this exact Agent. */
625
+ closingTeardownFor(agent) {
626
+ if (this.draining)
627
+ return 'manager';
628
+ const root = this.rootOf(agent);
629
+ return this.closingRoots.has(root) ? root : undefined;
630
+ }
631
+ /** Share one capacity pool across every instance in a subagent tree. */
632
+ rootPool(parent) {
633
+ let pool = this.rootPools.get(parent);
634
+ if (pool === undefined) {
635
+ pool = new ActivationPool();
636
+ this.rootPools.set(parent, pool);
637
+ }
638
+ return pool;
639
+ }
640
+ /** Perform one tracked materialization through publication or rollback. */
641
+ async materializeTracked(inputs, releaseSlot) {
642
+ const { provider, root } = inputs;
643
+ inputs.signal.throwIfAborted();
644
+ let resource;
645
+ if (inputs.kind === 'external') {
646
+ const controller = new AbortController();
647
+ const cancelStartup = () => { controller.abort(inputs.signal.reason); };
648
+ inputs.signal.addEventListener('abort', cancelStartup, { once: true });
649
+ try {
650
+ const run = await inputs.start(controller.signal);
651
+ resource = { kind: 'external', run, controller };
652
+ void run.result.catch(() => undefined);
653
+ try {
654
+ this.assertChildIdAvailable(run.id);
655
+ }
656
+ catch (error) {
657
+ await run.dispose();
658
+ throw error;
659
+ }
660
+ }
661
+ finally {
662
+ inputs.signal.removeEventListener('abort', cancelStartup);
663
+ }
664
+ }
665
+ else {
666
+ const { create, childId } = inputs;
667
+ let structured;
668
+ const setup = async (childCtx, child) => {
669
+ // Only fresh creation appends the descriptor and delegated policy after
670
+ // the inherited marker; a cold resume replays those persisted events.
671
+ // Fresh creation knows the captured policy; a resumed child reads the
672
+ // policy its replayed log already folded.
673
+ const approvalPolicy = create?.delegatedPolicies.approvalPolicy
674
+ ?? childCtx.get('approval')?.effectivePolicy(child.session);
675
+ if (inputs.composition.agentPreset !== undefined) {
676
+ await applyChildComposition(childCtx, create?.parent, inputs.composition, approvalPolicy);
677
+ }
678
+ if (create !== undefined) {
679
+ if (inputs.composition.agentPreset !== undefined) {
680
+ child.session.append('agent-preset/selected', { agentPreset: inputs.composition.agentPreset });
681
+ }
682
+ child.session.append('subagent/descriptor', create.descriptor);
683
+ appendDelegatedPolicyOverrides(child.session, create.delegatedPolicies);
684
+ const workingDirectory = childCtx.get('workingDirectory');
685
+ if (workingDirectory === undefined)
686
+ throw new Error('local subagents require the working-directory service');
687
+ await workingDirectory.set(child, create.cwd, inputs.signal);
688
+ }
689
+ if (inputs.composition.agentPreset === undefined) {
690
+ await applyChildComposition(childCtx, create?.parent, inputs.composition, approvalPolicy);
691
+ }
692
+ if (inputs.outputSchema !== undefined) {
693
+ structured = attachStructuredRuntime(childCtx, inputs.outputSchema, () => child.inbox.nextStep.length === 0
694
+ && child.inbox.nextTurn.length === 0);
695
+ }
696
+ };
697
+ const handle = create === undefined
698
+ ? await this.ownerCtx.agents.resume({
699
+ resumeSessionId: childId,
700
+ agentOptions: inputs.agentOptions,
701
+ signal: inputs.signal,
702
+ setup,
703
+ })
704
+ : await this.ownerCtx.agents.create({
705
+ sessionId: childId,
706
+ meta: create.meta,
707
+ ...(create.seed === undefined ? {} : { seed: create.seed }),
708
+ inheritedEventCount: create.inheritedEventCount,
709
+ agentOptions: inputs.agentOptions,
710
+ signal: inputs.signal,
711
+ setup,
712
+ });
713
+ resource = {
714
+ kind: 'local',
715
+ handle,
716
+ outputStart: handle.agent.session.seq,
717
+ structured,
718
+ failureAt: undefined,
719
+ poke: Promise.withResolvers(),
720
+ omitClosingText: false,
721
+ };
722
+ }
723
+ const childId = resource.kind === 'local' ? resource.handle.agent.id : resource.run.id;
724
+ const activation = {
725
+ releaseSlot,
726
+ childId,
727
+ runId: SubagentRunId(randomUUID()),
728
+ provider,
729
+ ...resource,
730
+ root,
731
+ ancestors: inputs.ancestors,
732
+ delivery: inputs.delivery ?? 'parent',
733
+ result: Promise.withResolvers(),
734
+ closing: undefined,
735
+ accepted: false,
736
+ };
737
+ void activation.result.promise.catch(() => undefined);
738
+ this.resident.set(childId, activation);
739
+ try {
740
+ this.assertAdmitting(root);
741
+ inputs.signal.throwIfAborted();
742
+ const wakeSettlement = () => { this.wake(activation); };
743
+ if (activation.kind === 'local') {
744
+ observeLocalFailure(activation, wakeSettlement);
745
+ activation.handle.agent.ctx.on('agent/inbox/inserted', wakeSettlement);
746
+ activation.handle.agent.ctx.on('agent/inbox/claimed', () => {
747
+ // Claimed input means the closing text may report work the child's
748
+ // last message to its parent did not.
749
+ activation.omitClosingText = false;
750
+ wakeSettlement();
751
+ });
752
+ activation.handle.agent.ctx.on('agent/inbox/discarded', wakeSettlement);
753
+ // A tool call logged after the message also voids it; logged calls
754
+ // cannot be skipped by tool policy. `send_message` is not concurrency
755
+ // safe, so a later call in its step is logged after it executes.
756
+ activation.handle.agent.ctx.on('session/event', (_session, event) => {
757
+ if (event.type === 'tool/call')
758
+ activation.omitClosingText = false;
759
+ });
760
+ }
761
+ // Listeners may read the resident Agent or drain it immediately. Publish
762
+ // after residency and inbox listeners, as the last rollback-protected step.
763
+ this.host.emitLifecycle('subagent/start', this.runInfo(activation), root);
764
+ }
765
+ catch (error) {
766
+ /* v8 ignore next -- rollback failure must not mask the admission failure
767
+ * that prevented this operation from returning an accepted message id. */
768
+ await this.rollbackUnpublished(activation, error).catch(() => undefined);
769
+ throw error;
770
+ }
771
+ return activation;
772
+ }
773
+ /** Identity shared by one residency epoch's start/end pair, including cold resumes. */
774
+ runInfo(activation) {
775
+ return {
776
+ runId: activation.runId,
777
+ provider: activation.provider,
778
+ parentSessionId: activation.ancestors.at(-1),
779
+ id: activation.childId,
780
+ local: activation.kind === 'local',
781
+ };
782
+ }
783
+ /** Record acceptance and begin settlement observation before returning the receipt. */
784
+ accept(activation) {
785
+ activation.accepted = true;
786
+ if (activation.kind === 'local') {
787
+ this.watchLocalSettlement(activation);
788
+ }
789
+ else {
790
+ const finish = () => this.close(activation, () => this.finishDisposal(activation, false));
791
+ void activation.run.result.then(finish, finish).catch((error) => { this.reportTeardownFailure(activation, error); });
792
+ }
793
+ }
794
+ /** Release an Activation whose start edge was not published. */
795
+ rollbackUnpublished(activation, error) {
796
+ return this.close(activation, async () => {
797
+ try {
798
+ await (activation.kind === 'local' ? activation.handle.dispose() : activation.run.dispose());
799
+ }
800
+ finally {
801
+ this.resident.delete(activation.childId);
802
+ activation.releaseSlot();
803
+ activation.result.reject(error);
804
+ }
805
+ });
806
+ }
807
+ /** Let a settlement watcher re-check residency after relevant state changes. */
808
+ wake(activation) {
809
+ if (activation.kind === 'external')
810
+ return;
811
+ activation.poke.resolve();
812
+ activation.poke = Promise.withResolvers();
813
+ }
814
+ /** Follow one Activation to natural settlement. */
815
+ watchLocalSettlement(activation) {
816
+ void (async () => {
817
+ while (true) {
818
+ const idleObservation = activation.poke;
819
+ await activation.handle.agent.whenIdle();
820
+ if (activation.closing !== undefined)
821
+ return;
822
+ const readiness = await this.locks.run(activation.childId, () => Promise.resolve(this.settlementState(activation, idleObservation)));
823
+ if (readiness === 'closed')
824
+ return;
825
+ if (readiness === 'retry')
826
+ continue;
827
+ if (readiness === 'wait') {
828
+ await idleObservation.promise;
829
+ continue;
830
+ }
831
+ const finalSeq = activation.handle.agent.session.seq;
832
+ await this.flushFinalState(activation);
833
+ const attempt = await this.locks.run(activation.childId, () => {
834
+ const state = this.settlementState(activation, idleObservation);
835
+ if (state !== 'ready')
836
+ return Promise.resolve(state);
837
+ if (activation.handle.agent.session.seq !== finalSeq) {
838
+ return Promise.resolve('retry');
839
+ }
840
+ let done;
841
+ try {
842
+ void activation.handle.agent.runMaintenance(() => {
843
+ done = this.close(activation, () => this.finishDisposal(activation, false));
844
+ return Promise.resolve();
845
+ });
846
+ }
847
+ catch (_error) {
848
+ // Another activity already owns the Agent's idle phase.
849
+ return Promise.resolve('retry');
850
+ }
851
+ return Promise.resolve({ done });
852
+ });
853
+ if (typeof attempt === 'string') {
854
+ if (attempt === 'closed')
855
+ return;
856
+ continue;
857
+ }
858
+ try {
859
+ await attempt.done;
860
+ }
861
+ catch (error) {
862
+ this.reportTeardownFailure(activation, error);
863
+ }
864
+ return;
865
+ }
866
+ })();
867
+ }
868
+ /** Report disposal failure once after the natural settlement observer finishes. */
869
+ reportTeardownFailure(activation, error) {
870
+ this.ctx.logger.warn(`subagent "${activation.childId}" activation teardown failed: ${failureMessage(error)}`);
871
+ }
872
+ /** Check pending input before closing admission. */
873
+ settlementState(activation, observation) {
874
+ if (activation.closing !== undefined)
875
+ return 'closed';
876
+ if (activation.poke !== observation)
877
+ return 'retry';
878
+ const inbox = activation.handle.agent.inbox;
879
+ if (activation.failureAt === undefined && (inbox.nextTurn.length > 0 || inbox.nextStep.length > 0))
880
+ return 'wait';
881
+ return 'ready';
882
+ }
883
+ /** Stop and release one instance without affecting its creation descendants. */
884
+ async finishDisposal(activation, stop) {
885
+ this.wake(activation);
886
+ const { childId } = activation;
887
+ const failures = [];
888
+ let result = { output: [], stopReason: 'error' };
889
+ let resultFailure;
890
+ let externalDisposal;
891
+ const child = activationAgent(activation);
892
+ const detail = (error) => child === undefined
893
+ ? failureMessage(error)
894
+ : errorChain(error);
895
+ try {
896
+ if (activation.kind === 'external') {
897
+ if (stop) {
898
+ activation.controller.abort({ kind: 'parent' });
899
+ // Some backends settle their result only when disposal begins.
900
+ externalDisposal = Promise.resolve().then(() => activation.run.dispose());
901
+ void externalDisposal.catch(() => undefined);
902
+ }
903
+ result = await activation.run.result;
904
+ }
905
+ else {
906
+ if (stop) {
907
+ activation.handle.agent.cancel({ kind: 'parent' });
908
+ const idle = activation.handle.agent.whenIdle();
909
+ await idle;
910
+ await this.flushFinalState(activation);
911
+ }
912
+ result = captureLocalResult(activation);
913
+ }
914
+ }
915
+ catch (error) {
916
+ resultFailure = { error };
917
+ failures.push(new SubagentError(`subagent "${childId}" activation teardown failed: ${detail(error)}`, 'ACTIVATION_TEARDOWN_FAILED', { cause: error }));
918
+ }
919
+ try {
920
+ if (activation.kind === 'local')
921
+ await activation.handle.dispose();
922
+ else
923
+ await (externalDisposal ?? activation.run.dispose());
924
+ }
925
+ catch (error) {
926
+ failures.push(new SubagentError(`subagent "${childId}" activation handle disposal failed: ${detail(error)}`, 'ACTIVATION_TEARDOWN_FAILED', { cause: error }));
927
+ }
928
+ let failure;
929
+ if (failures.length === 1) {
930
+ failure = failures[0];
931
+ }
932
+ else if (failures.length > 1) {
933
+ failure = new SubagentError(`subagent "${childId}" activation teardown failed at ${failures.length} boundaries: `
934
+ + failures.map(item => detail(item)).join('; '), 'ACTIVATION_TEARDOWN_FAILED', { cause: new AggregateError(failures) });
935
+ }
936
+ this.resident.delete(childId);
937
+ activation.releaseSlot();
938
+ const terminal = failure === undefined ? result : { output: [], stopReason: 'error' };
939
+ this.notifySettlement(activation, result);
940
+ // The sole end publication follows resource release and parent notification,
941
+ // before result delivery. Cleanup failure changes the event, not the captured result.
942
+ this.host.emitLifecycle('subagent/end', {
943
+ ...this.runInfo(activation),
944
+ stopReason: terminal.stopReason,
945
+ ...terminal.output.length === 0 ? {} : { lastAssistantMessage: terminal.output },
946
+ }, activation.root);
947
+ if (resultFailure === undefined)
948
+ activation.result.resolve(result);
949
+ else
950
+ activation.result.reject(resultFailure.error);
951
+ if (failure !== undefined)
952
+ throw failure;
953
+ }
954
+ /** Deliver the captured execution result to the durable direct parent after cleanup. */
955
+ notifySettlement(activation, terminal) {
956
+ if (!activation.accepted || activation.delivery === 'caller')
957
+ return;
958
+ try {
959
+ const parentId = activation.ancestors.at(-1);
960
+ const parent = this.ctx.agents.get(parentId);
961
+ if (parent === undefined || this.rootOf(parent) !== activation.root)
962
+ return;
963
+ const message = createSettlementMessage(activation.childId, terminal, activation.kind === 'local', activation.kind === 'local' && activation.omitClosingText);
964
+ if (this.closingTeardownFor(parent) !== undefined) {
965
+ parent.inject(message);
966
+ return;
967
+ }
968
+ this.sendWaking(parent, message, parent.status === 'idle' ? 'queue' : 'steer');
969
+ }
970
+ catch (error) {
971
+ this.ctx.logger.warn(`subagent "${activation.childId}" settlement notice was not delivered to its parent: `
972
+ + errorChain(error));
973
+ }
974
+ }
975
+ /** Request a best-effort final session flush before closing natural-settlement admission. */
976
+ async flushFinalState(activation) {
977
+ try {
978
+ await activation.handle.agent.ctx.sessions.flush(activation.handle.agent.session);
979
+ }
980
+ catch (error) {
981
+ this.ctx.logger.warn(`subagent "${activation.childId}" best-effort final session flush failed; `
982
+ + `the persisted state may be unavailable or stale on resume: ${errorChain(error)}`);
983
+ }
984
+ }
985
+ /** Return capabilities bound to an exact activation instead of a future same-id instance. */
986
+ receipt(activation) {
987
+ return {
988
+ childId: activation.childId,
989
+ result: activation.result.promise,
990
+ dispose: () => this.dispose(activation),
991
+ };
992
+ }
993
+ /** Serialize target restoration and inbox admission; closure is awaited outside the lock. */
994
+ async deliverToMember(root, childId, content, options) {
995
+ while (true) {
996
+ this.assertAdmitting(root);
997
+ options.signal.throwIfAborted();
998
+ const result = await this.locks.run(childId, async () => {
999
+ if (childId === root.id) {
1000
+ if (contentHasImage(content))
1001
+ await this.assertImageCapable(root, options.signal);
1002
+ options.signal.throwIfAborted();
1003
+ options.authorize?.();
1004
+ this.assertAdmitting(root);
1005
+ const message = createUserMessage({ content, source: options.source });
1006
+ this.sendWaking(root, message, options.delivery);
1007
+ options.onAccepted?.();
1008
+ return { messageId: message.id };
1009
+ }
1010
+ const activation = this.resident.get(childId);
1011
+ if (activation === undefined) {
1012
+ const restored = await this.restoreTarget(root, childId, options.signal);
1013
+ return { messageId: await this.submitMaterialized(restored, content, options, root) };
1014
+ }
1015
+ if (activation.root !== root)
1016
+ throw new SubagentError('target belongs to another subagent tree', 'UNAUTHORIZED');
1017
+ requireLocalActivation(activation);
1018
+ if (activation.closing === undefined && contentHasImage(content)) {
1019
+ await this.assertImageCapable(requireLocalActivation(activation).handle.agent, options.signal);
1020
+ }
1021
+ if (activation.closing !== undefined)
1022
+ return { closing: activation.closing };
1023
+ const messageId = this.submitAdmitted(activation, content, options, root);
1024
+ return { messageId };
1025
+ });
1026
+ if ('messageId' in result)
1027
+ return result.messageId;
1028
+ await Promise.allSettled([result.closing]);
1029
+ }
1030
+ }
1031
+ /** Read continuation history, preserving cancellation and reporting unavailable storage. */
1032
+ async observeContinuation(childId, signal) {
1033
+ const query = this.requireSessionQuery();
1034
+ try {
1035
+ return await query.observeSession(childId, { signal });
1036
+ }
1037
+ catch (error) {
1038
+ signal.throwIfAborted();
1039
+ throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
1040
+ }
1041
+ }
1042
+ /** Reconstruct only the target under its subagent tree root; the caller owns acceptance or rollback. */
1043
+ async restoreTarget(root, childId, signal) {
1044
+ return this.trackStartup(root, signal, async (signal) => {
1045
+ const env_3 = { stack: [], error: void 0, hasError: false };
1046
+ try {
1047
+ const source = __addDisposableResource(env_3, await this.observeContinuation(childId, signal), false);
1048
+ this.assertAdmitting(root);
1049
+ const ownEvents = source.events.slice(source.inheritedEventCount);
1050
+ const descriptor = foldSubagentDescriptor(ownEvents);
1051
+ if (descriptor === undefined || descriptor.mode !== 'continuable') {
1052
+ throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed; choose a different target`, 'NOT_RESUMABLE');
1053
+ }
1054
+ const { ancestors } = await this.resolveCreationAncestry(source, signal, root);
1055
+ let activation;
1056
+ try {
1057
+ const projectedPreset = source.projections?.values.agentPreset;
1058
+ if (projectedPreset === undefined && source.events.some(event => event.type === 'agent-preset/selected')) {
1059
+ throw new SubagentError(`subagent "${childId}" has no agent preset projection for restoration`, 'NOT_RESUMABLE');
1060
+ }
1061
+ const agentPreset = projectedPreset ?? source.header.agentPreset;
1062
+ activation = await this.materialize({
1063
+ kind: 'local',
1064
+ childId,
1065
+ provider: descriptor.provider,
1066
+ root,
1067
+ ancestors,
1068
+ agentOptions: {
1069
+ ...descriptor.agentProvider !== undefined ? { provider: descriptor.agentProvider } : {},
1070
+ ...descriptor.agentModel !== undefined ? { model: descriptor.agentModel } : {},
1071
+ ...descriptor.agentReasoningEffort !== undefined
1072
+ ? { reasoningEffort: ReasoningEffortId(descriptor.agentReasoningEffort) }
1073
+ : {},
1074
+ },
1075
+ composition: {
1076
+ agentPreset: agentPreset ?? undefined,
1077
+ persona: descriptor.persona,
1078
+ toolFilter: descriptor.toolFilter,
1079
+ },
1080
+ signal,
1081
+ });
1082
+ }
1083
+ catch (error) {
1084
+ signal.throwIfAborted();
1085
+ if (error instanceof SubagentError)
1086
+ throw error;
1087
+ throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
1088
+ }
1089
+ return activation;
1090
+ }
1091
+ catch (e_3) {
1092
+ env_3.error = e_3;
1093
+ env_3.hasError = true;
1094
+ }
1095
+ finally {
1096
+ __disposeResources(env_3);
1097
+ }
1098
+ });
1099
+ }
1100
+ /** Admit a materialized child, commit its creation fact, and release it on failure. */
1101
+ async submitMaterialized(activation, content, options, parent, commit) {
1102
+ try {
1103
+ if (contentHasImage(content)) {
1104
+ await this.assertImageCapable(requireLocalActivation(activation).handle.agent, options.signal);
1105
+ if (activation.closing !== undefined) {
1106
+ throw new SubagentError(`subagent "${activation.childId}" is closing`, 'ACTIVATION_CLOSING');
1107
+ }
1108
+ }
1109
+ const messageId = this.submitAdmitted(activation, content, options, parent);
1110
+ commit?.();
1111
+ this.accept(activation);
1112
+ return messageId;
1113
+ }
1114
+ catch (error) {
1115
+ try {
1116
+ await this.dispose(activation);
1117
+ }
1118
+ catch (cleanupError) {
1119
+ this.ctx.logger.warn(`subagent continuation: disposal after admission or catalog append failure also failed: ${String(cleanupError)}`);
1120
+ }
1121
+ throw error;
1122
+ }
1123
+ }
1124
+ /** Build and submit one message across the final synchronous admission cutoff. */
1125
+ submitAdmitted(activation, content, options, parent) {
1126
+ const message = createUserMessage({ content, source: options.source });
1127
+ options.signal.throwIfAborted();
1128
+ options.authorize?.();
1129
+ this.assertAdmitting(parent);
1130
+ try {
1131
+ this.deliver(activation, message, options.delivery);
1132
+ }
1133
+ finally {
1134
+ this.wake(activation);
1135
+ }
1136
+ options.onAccepted?.();
1137
+ return message.id;
1138
+ }
1139
+ /** Refuse image content for a child whose fixed model accepts text only. */
1140
+ async assertImageCapable(agent, signal) {
1141
+ const { provider, model } = agent.options;
1142
+ if (provider === undefined || model === undefined)
1143
+ return;
1144
+ const llm = this.ctx.get('llm');
1145
+ /* v8 ignore next -- without an LLM registry, delivery defers to projection. */
1146
+ if (llm === undefined)
1147
+ return;
1148
+ const info = await llm.resolveModelInfo(provider, model, signal);
1149
+ if (info.inputModalities !== undefined && !info.inputModalities.includes('image')) {
1150
+ throw new SubagentError(`Model "${model}" does not support image input.`, 'MODEL_DOES_NOT_SUPPORT_IMAGES');
1151
+ }
1152
+ }
1153
+ /** Resolve the persistence service continuable children require, or fail loud. */
1154
+ requirePersistence() {
1155
+ const persistence = this.ctx.get('sessionPersistence');
1156
+ if (persistence === undefined) {
1157
+ throw new SubagentError('continuable subagents require session persistence (load a dsh-session-persistence backend)', 'PERSISTENCE_UNAVAILABLE');
1158
+ }
1159
+ return persistence;
1160
+ }
1161
+ /** Resolve the Session query service used for cold child observations. */
1162
+ requireSessionQuery() {
1163
+ const query = this.ctx.get('sessionQuery');
1164
+ if (query === undefined) {
1165
+ throw new SubagentError('continuable subagents require session query (load @deepseek-ai/dsh-session-query)', 'CONTINUATION_UNAVAILABLE');
1166
+ }
1167
+ return query;
1168
+ }
1169
+ }
1170
+ export default SubagentManager;
1171
+ //# sourceMappingURL=manager.js.map