@xneog/dsh-subagent 0.1.0 → 0.1.3-alpha.1

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 (41) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +108 -76
  3. package/README.zh.md +112 -80
  4. package/lib/index.js +1258 -718
  5. package/lib/typert.host.d.ts +3 -0
  6. package/lib/typert.host.js +923 -0
  7. package/lib/typert.remote-client.d.ts +27 -0
  8. package/lib/typert.remote-client.js +159 -0
  9. package/lib/types/assistant-output.d.ts +3 -3
  10. package/lib/types/assistant-output.js +8 -4
  11. package/lib/types/child-agent.d.ts +16 -5
  12. package/lib/types/child-agent.js +51 -13
  13. package/lib/types/client.d.ts +2 -1
  14. package/lib/types/client.js +1 -1
  15. package/lib/types/continuation.d.ts +100 -72
  16. package/lib/types/continuation.js +439 -169
  17. package/lib/types/control-types.d.ts +144 -0
  18. package/lib/types/control-types.js +9 -0
  19. package/lib/types/control.d.ts +67 -0
  20. package/lib/types/control.js +115 -0
  21. package/lib/types/descriptor-seed.d.ts +1 -1
  22. package/lib/types/descriptor-seed.js +1 -1
  23. package/lib/types/descriptor.d.ts +6 -1
  24. package/lib/types/descriptor.js +6 -2
  25. package/lib/types/index.d.ts +103 -69
  26. package/lib/types/index.js +436 -287
  27. package/lib/types/internal.d.ts +59 -0
  28. package/lib/types/internal.js +58 -0
  29. package/lib/types/lifecycle.js +4 -3
  30. package/lib/types/list-children.d.ts +12 -59
  31. package/lib/types/list-children.js +166 -101
  32. package/lib/types/out-of-process.d.ts +5 -2
  33. package/lib/types/out-of-process.js +42 -4
  34. package/lib/types/projection-types.d.ts +4 -3
  35. package/lib/types/projection.d.ts +55 -8
  36. package/lib/types/projection.js +33 -17
  37. package/lib/types/run-settlement.js +17 -6
  38. package/lib/types/types.d.ts +25 -0
  39. package/package.json +67 -37
  40. package/lib/types/activation-setup-registry.d.ts +0 -57
  41. package/lib/types/activation-setup-registry.js +0 -148
@@ -20,14 +20,67 @@
20
20
  *
21
21
  * @module @xneog/dsh-subagent
22
22
  */
23
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
24
+ if (value !== null && value !== void 0) {
25
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
26
+ var dispose, inner;
27
+ if (async) {
28
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
29
+ dispose = value[Symbol.asyncDispose];
30
+ }
31
+ if (dispose === void 0) {
32
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
33
+ dispose = value[Symbol.dispose];
34
+ if (async) inner = dispose;
35
+ }
36
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
37
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
38
+ env.stack.push({ value: value, dispose: dispose, async: async });
39
+ }
40
+ else if (async) {
41
+ env.stack.push({ async: true });
42
+ }
43
+ return value;
44
+ };
45
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
46
+ return function (env) {
47
+ function fail(e) {
48
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
49
+ env.hasError = true;
50
+ }
51
+ var r, s = 0;
52
+ function next() {
53
+ while (r = env.stack.pop()) {
54
+ try {
55
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
56
+ if (r.dispose) {
57
+ var result = r.dispose.call(r.value);
58
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
59
+ }
60
+ else s |= 1;
61
+ }
62
+ catch (e) {
63
+ fail(e);
64
+ }
65
+ }
66
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
67
+ if (env.hasError) throw env.error;
68
+ }
69
+ return next();
70
+ };
71
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
72
+ var e = new Error(message);
73
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
74
+ });
23
75
  import { randomUUID } from 'node:crypto';
24
- import { boundContextSummary, createUserMessage, errorChain } from '@xneog/dsh-llm';
25
- import { SessionId } from '@xneog/dsh-session';
76
+ import { brandString } from '@xneog/dsh-brand';
77
+ import { ReasoningEffortId, boundContextSummary, contentHasImage, createUserMessage, errorChain } from '@xneog/dsh-llm';
78
+ import { SessionLogOffset } from '@xneog/dsh-session';
26
79
  import { foldSubagentDescriptor, snapshotSubagentDescriptor } from "./descriptor.js";
27
80
  import { appendDelegatedPolicyOverrides, applyChildComposition, captureDelegatedPolicyOverrides, childSessionMeta, resolveChildAgentOptions, resolveChildDepth, } from "./child-agent.js";
28
81
  import { assertSubagentMaxDepth } from "./depth.js";
29
- import { seedDescriptorTurn } from "./descriptor-seed.js";
30
82
  import { SubagentError } from "./error.js";
83
+ import { isAdjacentAgentSendMessageTool } from "./internal.js";
31
84
  /**
32
85
  * Read one Activation's current disposal transaction. This indirection exists
33
86
  * because TypeScript would otherwise narrow repeated reads of the mutable field
@@ -38,6 +91,39 @@ import { SubagentError } from "./error.js";
38
91
  function disposalOf(activation) {
39
92
  return activation.disposal;
40
93
  }
94
+ /** Build durable attribution for one adjacent-Agent message. */
95
+ function agentMessageSource(sender) {
96
+ return {
97
+ kind: 'agent-message',
98
+ form: 'relay',
99
+ senderSessionId: sender.id,
100
+ };
101
+ }
102
+ /** Build the model-visible and durable representation of one adjacent-Agent message. */
103
+ function agentMessage(sender, content) {
104
+ return createUserMessage({
105
+ content: [
106
+ { type: 'text', text: `Agent ${sender.id} sent a message:` },
107
+ ...content,
108
+ ],
109
+ source: agentMessageSource(sender),
110
+ });
111
+ }
112
+ /** Append adjacent-Agent return guidance to a continuable child's initial task. */
113
+ function continuableInitialPrompt(parentId, prompt) {
114
+ const encodedParentId = JSON.stringify(parentId);
115
+ return [
116
+ ...prompt,
117
+ {
118
+ type: 'text',
119
+ text: `Your parent agent id is ${encodedParentId}. Before you finish, send your result to that agent with `
120
+ + `send_message({ agent_id: ${encodedParentId}, message: "<self-contained result>" }). The parent shares `
121
+ + 'your workspace but does not automatically receive your transcript, tool output, or reasoning. Send '
122
+ + 'earlier messages as well when a finding changes what the parent should do next; sending a message '
123
+ + 'does not end your turn.',
124
+ },
125
+ ];
126
+ }
41
127
  /**
42
128
  * One line telling a parent that a background child is finished and why, in
43
129
  * the parent's own task vocabulary.
@@ -99,7 +185,6 @@ class ChildLock {
99
185
  export class SubagentContinuationManager {
100
186
  ctx;
101
187
  host;
102
- setupRegistry;
103
188
  /** Child session id → its live Activation. Process-local, never durable. */
104
189
  activations = new Map();
105
190
  /** Materializations admitted before drain, tracked through publication or rollback. */
@@ -115,10 +200,9 @@ export class SubagentContinuationManager {
115
200
  */
116
201
  closingScopes = new Map();
117
202
  draining = false;
118
- constructor(ctx, host, setupRegistry) {
203
+ constructor(ctx, host) {
119
204
  this.ctx = ctx;
120
205
  this.host = host;
121
- this.setupRegistry = setupRegistry;
122
206
  // Ordinary Cordis owner effects unwind in reverse registration order, which
123
207
  // cannot express the dynamic child graph. Register the private scope's
124
208
  // structural disposer FIRST and the drain SECOND, so reverse unwind invokes
@@ -154,68 +238,199 @@ export class SubagentContinuationManager {
154
238
  const request = spec.request;
155
239
  const parent = request.parent;
156
240
  this.assertAdmitting(parent);
157
- this.requirePersistence();
241
+ const persistence = this.requirePersistence();
158
242
  assertSubagentMaxDepth(request.maxDepth);
159
- const childId = SessionId(randomUUID());
243
+ const childId = spec.childId ?? brandString(randomUUID());
244
+ this.assertChildIdAvailable(childId);
160
245
  const childDepth = resolveChildDepth(parent, request.maxDepth);
161
246
  // Snapshot before any await: invalid descriptor JSON rejects the call
162
247
  // before a child exists, and the detached value is what reaches the log.
163
- const agentProvider = request.agentOptions?.provider ?? parent.options.provider;
164
- const agentModel = request.agentOptions?.model ?? parent.options.model;
248
+ const agentOptions = resolveChildAgentOptions(parent, request.agentOptions, childDepth);
249
+ const agentProvider = agentOptions.provider;
250
+ const agentModel = agentOptions.model;
251
+ const agentReasoningEffort = agentOptions.reasoningEffort;
165
252
  const descriptor = snapshotSubagentDescriptor({
166
253
  mode: 'continuable',
167
254
  provider: spec.provider,
168
255
  label: spec.label,
169
256
  ...agentProvider !== undefined ? { agentProvider } : {},
170
257
  ...agentModel !== undefined ? { agentModel } : {},
258
+ ...agentReasoningEffort !== undefined ? { agentReasoningEffort } : {},
171
259
  ...request.persona !== undefined ? { persona: request.persona } : {},
172
260
  ...request.toolFilter !== undefined ? { toolFilter: request.toolFilter } : {},
173
261
  });
174
262
  // Capture before the first await: a later parent switch belongs to the
175
263
  // parent's future, not to this child.
176
264
  const delegatedPolicies = captureDelegatedPolicyOverrides(parent);
177
- const prepared = await this.host.prepareContinuable(spec.provider, {
178
- sessionId: childId,
179
- parent,
180
- signal: spec.signal,
181
- });
182
- spec.signal.throwIfAborted();
183
- this.assertAdmitting(parent);
184
- const lineageSeedLength = prepared.seed?.length ?? 0;
185
- const seed = seedDescriptorTurn(childId, prepared.seed, descriptor);
186
- const messageId = await this.locks.run(childId, async () => {
187
- const activation = await this.materialize({
188
- childId,
189
- provider: spec.provider,
265
+ // Hold the parent's own Activation open across the establishment awaits:
266
+ // an idle continuation-managed parent must not settle while a caller is
267
+ // still creating its child, or the admitted delivery would find a stale
268
+ // parent identity. A turn-scoped delegation never needs this (the parent
269
+ // is `running`), but this service is also callable outside a turn.
270
+ const releaseHold = this.holdOwnership(parent, childId);
271
+ try {
272
+ const prepared = await this.host.prepareContinuable(spec.provider, {
273
+ sessionId: childId,
190
274
  parent,
191
- create: { seed, meta: childSessionMeta(parent, childDepth, lineageSeedLength), delegatedPolicies },
192
- agentOptions: resolveChildAgentOptions(parent, request.agentOptions, childDepth),
193
- composition: { persona: request.persona, toolFilter: request.toolFilter },
194
275
  signal: spec.signal,
195
276
  });
196
- return this.submitMaterialized(activation, request.prompt, { kind: 'user' }, parent, spec.signal);
277
+ spec.signal.throwIfAborted();
278
+ this.assertAdmitting(parent);
279
+ const inheritedEventCount = SessionLogOffset(prepared.seed?.length ?? 0);
280
+ const seed = prepared.seed;
281
+ const messageId = await this.locks.run(childId, async () => {
282
+ spec.signal.throwIfAborted();
283
+ this.assertAdmitting(parent);
284
+ this.assertChildIdAvailable(childId);
285
+ if (spec.childId !== undefined) {
286
+ const persisted = await persistence.stat(childId, { signal: spec.signal });
287
+ spec.signal.throwIfAborted();
288
+ this.assertAdmitting(parent);
289
+ this.assertChildIdAvailable(childId);
290
+ if (persisted !== undefined) {
291
+ throw new SubagentError(`subagent "${childId}" already exists`, 'DUPLICATE_CHILD');
292
+ }
293
+ }
294
+ const activation = await this.materialize({
295
+ childId,
296
+ provider: spec.provider,
297
+ parent,
298
+ create: {
299
+ seed,
300
+ meta: childSessionMeta(parent, childDepth, prepared.seed !== undefined),
301
+ inheritedEventCount,
302
+ delegatedPolicies,
303
+ descriptor,
304
+ },
305
+ agentOptions,
306
+ composition: { persona: request.persona, toolFilter: request.toolFilter },
307
+ signal: spec.signal,
308
+ });
309
+ return this.submitMaterialized(activation, isAdjacentAgentSendMessageTool(this.ctx.get('tools')?.get('send_message', activation.handle.agent))
310
+ ? continuableInitialPrompt(parent.id, request.prompt)
311
+ : request.prompt, { source: { kind: 'user' }, signal: spec.signal, delivery: 'queue' }, parent);
312
+ });
313
+ return { childId, messageId };
314
+ }
315
+ catch (error) {
316
+ releaseHold();
317
+ throw error;
318
+ }
319
+ }
320
+ /**
321
+ * Pre-register `childId` in a continuation-managed parent's owned set so the
322
+ * parent cannot settle while a caller is still establishing or resuming that
323
+ * child. Returns a releaser for the failure path; it removes only a hold
324
+ * this call added, and leaves ownership in place once a live Activation for
325
+ * the child exists (an admitted delivery owns it from then on). A parent
326
+ * without an Activation needs no hold: only this manager settles parents.
327
+ * @param parent - the live direct parent the operation is admitted under.
328
+ * @param childId - the durable child the operation addresses.
329
+ * @returns the failure-path releaser; a no-op when nothing was added.
330
+ * @throws {SubagentError} `ACTIVATION_CLOSING` when the parent's own
331
+ * disposal transaction is already open.
332
+ */
333
+ holdOwnership(parent, childId) {
334
+ const parentActivation = this.activations.get(parent.id);
335
+ if (parentActivation === undefined || parentActivation.handle.agent !== parent)
336
+ return () => { };
337
+ if (parentActivation.disposal !== undefined) {
338
+ throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, 'ACTIVATION_CLOSING');
339
+ }
340
+ if (parentActivation.ownedChildren.has(childId))
341
+ return () => { };
342
+ parentActivation.ownedChildren.add(childId);
343
+ return () => {
344
+ const live = this.activations.get(childId);
345
+ /* v8 ignore next 4 -- reaching this arm needs another delivery to establish the child
346
+ * between this operation's failure and its releaser running, which no test can schedule
347
+ * deterministically: the ownership edge then belongs to that live Activation, so the
348
+ * conservative keep leaves it for finishDisposal's releaseOwnership. */
349
+ if (live !== undefined && live.disposal === undefined)
350
+ return;
351
+ if (parentActivation.ownedChildren.delete(childId))
352
+ this.wake(parentActivation);
353
+ };
354
+ }
355
+ /** Reject one child identity already owned by a live Agent or Session. */
356
+ assertChildIdAvailable(childId) {
357
+ if (this.ctx.agents.get(childId) !== undefined || this.ctx.get('sessions')?.get(childId) !== undefined) {
358
+ throw new SubagentError(`subagent "${childId}" already exists`, 'DUPLICATE_CHILD');
359
+ }
360
+ }
361
+ /**
362
+ * Deliver one model-authored message to a direct continuable child or to the
363
+ * sender's direct parent. Both directions use Steer: a running target admits
364
+ * the message at its nearest step boundary, while an idle target starts a
365
+ * turn. A missing direct child cold-resumes through the ordinary continuation
366
+ * lifecycle. The caller signal owns the operation only until inbox acceptance.
367
+ * @param sender - exact live Agent authorizing and originating the message.
368
+ * @param targetId - durable direct-parent or direct-child session id.
369
+ * @param content - model-authored content to deliver.
370
+ * @param options - caller cancellation before acceptance.
371
+ * @returns the accepted message's inbox id.
372
+ * @throws when adjacency, availability, or admission rejects delivery.
373
+ */
374
+ async sendMessage(sender, targetId, content, options) {
375
+ if (this.ctx.agents.get(sender.id) !== sender) {
376
+ throw new SubagentError('message delivery requires the exact live sender agent', 'UNAUTHORIZED');
377
+ }
378
+ this.assertAdmitting(sender);
379
+ const senderActivation = this.activations.get(sender.id);
380
+ if (senderActivation !== undefined
381
+ && senderActivation.handle.agent === sender
382
+ && senderActivation.parentSession === targetId) {
383
+ options.signal.throwIfAborted();
384
+ return this.sendToParent(senderActivation, sender, content);
385
+ }
386
+ if (sender.session.header.parentSession === targetId) {
387
+ throw new SubagentError(`agent "${sender.id}" is not a resident continuable child and cannot send to parent "${targetId}"`, 'UNAUTHORIZED');
388
+ }
389
+ return this.deliverToChild(sender, targetId, content, {
390
+ signal: options.signal,
391
+ delivery: 'steer',
197
392
  });
198
- return { childId, messageId };
199
393
  }
200
394
  /**
201
- * Deliver one later message to a known continuable child as its next FIFO
202
- * turn. Routing depends only on Activation residency: a `running` Activation
203
- * enqueues, a `waiting` one wakes the same Agent, and an absent one
204
- * cold-resumes a new Activation from the persisted Session. The Agent inbox
205
- * is the only queue, so every accepted message has one observable order.
206
- *
207
- * The caller signal owns lookup, materialization, and admission only until
208
- * inbox acceptance; afterwards the accepted turn cannot be cancelled through
209
- * this service.
210
- * @param parent - the exact live direct parent authorizing this delivery.
211
- * @param childId - the durable child session id.
212
- * @param content - the user-role content to deliver.
213
- * @param options - the message source fields and caller cancellation.
395
+ * Queue one human-authored prompt as a distinct direct-child turn.
396
+ * @param parent - exact live direct parent authorizing delivery.
397
+ * @param childId - durable direct-child session id.
398
+ * @param content - human-authored content to deliver.
399
+ * @param source - durable host-protocol provenance.
400
+ * @param signal - caller cancellation before inbox acceptance.
401
+ * @returns the accepted message's inbox id.
402
+ */
403
+ async queuePrompt(parent, childId, content, source, signal) {
404
+ return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'queue' });
405
+ }
406
+ /**
407
+ * Steer one host-authored prompt to a direct continuable child.
408
+ * @param parent - exact live direct parent authorizing delivery.
409
+ * @param childId - durable direct-child session id.
410
+ * @param content - host-authored content to deliver.
411
+ * @param source - durable host-protocol provenance.
412
+ * @param signal - caller cancellation before inbox acceptance.
214
413
  * @returns the accepted message's inbox id.
215
- * @throws when parent authority, availability, or admission rejects the delivery.
216
414
  */
217
- async followup(parent, childId, content, options) {
415
+ async steerPrompt(parent, childId, content, source, signal) {
416
+ return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'steer' });
417
+ }
418
+ /** Route one parent-originated delivery through residency and cold resume. */
419
+ async deliverToChild(parent, childId, content, options) {
218
420
  this.assertAdmitting(parent);
421
+ // Same hold as `startContinuable`: an idle continuation-managed parent
422
+ // must not settle underneath a cold resume it is authorizing.
423
+ const releaseHold = this.holdOwnership(parent, childId);
424
+ try {
425
+ return await this.deliverFollowup(parent, childId, content, options);
426
+ }
427
+ catch (error) {
428
+ releaseHold();
429
+ throw error;
430
+ }
431
+ }
432
+ /** The delivery loop behind {@link deliverToChild}, run under the parent hold. */
433
+ async deliverFollowup(parent, childId, content, options) {
219
434
  while (true) {
220
435
  const live = await this.locks.run(childId, async () => {
221
436
  const activation = this.activations.get(childId);
@@ -223,14 +438,27 @@ export class SubagentContinuationManager {
223
438
  return this.coldResume(parent, childId, content, options);
224
439
  // A delivery that arrives after the disposal transaction began must not
225
440
  // reach a handle being torn down; wait for release, then cold-resume.
441
+ const disposal = activation.disposal;
226
442
  /* v8 ignore next 3 -- the send-versus-dispose cutoff: reaching this arm needs a
227
443
  * delivery to observe the transaction inside the same critical section that opened it,
228
444
  * which no test can schedule deterministically. The behavior is covered end-to-end by
229
445
  * "cold-resumes a delivery that lost the race with final disposal". */
230
- if (activation.disposal !== undefined) {
231
- return activation.disposal.then(() => undefined, () => undefined);
446
+ if (disposal !== undefined) {
447
+ return disposal.then(() => undefined, () => undefined);
448
+ }
449
+ // Text-only delivery stays await-free, so the disposal-cutoff check
450
+ // above and the submit share one critical window. The image path
451
+ // awaits a capability read, so it re-checks the cutoff afterwards; a
452
+ // disposal that began during the read is waited out and retried like
453
+ // one observed on entry.
454
+ if (contentHasImage(content)) {
455
+ await this.assertImageCapable(activation.handle.agent, options.signal);
456
+ if (activation.disposal !== undefined) {
457
+ await Promise.allSettled([activation.disposal]);
458
+ return undefined;
459
+ }
232
460
  }
233
- return this.submitAdmitted(activation, content, options.source, parent, options.signal);
461
+ return this.submitAdmitted(activation, content, options, parent);
234
462
  });
235
463
  /* v8 ignore start -- only the lost-cutoff arm above returns undefined, so only that
236
464
  * race reaches the retry below, which then cold-resumes a new Activation. */
@@ -291,75 +519,26 @@ export class SubagentContinuationManager {
291
519
  return;
292
520
  activation.handle.agent.cancel(authority.kind === 'user' ? { kind: 'user' } : { kind: 'parent' }, { keepInbox: true });
293
521
  }
294
- /**
295
- * Deliver explicitly selected content from one resident continuable child to
296
- * its durable direct parent. Sender authorization, parent resolution, and
297
- * send acceptance share one no-await span. Reporting neither concludes the
298
- * child's turn nor changes its Activation lifetime.
299
- * @param child - exact live reporting child; this is the authority credential.
300
- * @param content - selected model-facing content.
301
- * @param options - scheduling policy and pre-acceptance cancellation.
302
- * @returns the stable identity of the message accepted by the parent.
303
- * @throws {SubagentError} when the sender is unauthorized, the parent is not
304
- * live, or continuation admission is closing.
305
- */
306
- // oxlint-disable-next-line typescript/require-await -- keep rejection semantics without yielding during admission
307
- async reportFrom(child, content, options) {
308
- options.signal.throwIfAborted();
309
- this.assertAdmitting(child);
310
- const activation = this.authorizeReporter(child);
311
- const parent = this.resolveReportParent(child);
312
- return this.deliverReport(activation, parent, content, options.delivery);
313
- }
314
- /** Authorize only the exact Agent of one resident Activation. */
315
- authorizeReporter(child) {
316
- const activation = this.activations.get(child.id);
317
- if (activation === undefined || activation.handle.agent !== child) {
318
- throw new SubagentError(`agent "${child.id}" is not a live continuable subagent and cannot report`, 'UNAUTHORIZED');
319
- }
320
- /* v8 ignore next 6 -- only a synchronous re-entrant disposer can open this
321
- * transaction between exact-agent authorization and this no-await cutoff. */
522
+ /** Deliver one resident continuable child's message to its live direct parent. */
523
+ sendToParent(activation, sender, content) {
524
+ /* v8 ignore next 6 -- only synchronous re-entrant teardown can open this
525
+ * transaction between exact-agent authorization and this no-await span. */
322
526
  if (activation.disposal !== undefined) {
323
- throw new SubagentError(`subagent "${child.id}" activation is being disposed; the report was not delivered`, 'ACTIVATION_CLOSING');
527
+ throw new SubagentError(`subagent "${sender.id}" activation is being disposed; the message was not delivered`, 'ACTIVATION_CLOSING');
324
528
  }
325
- return activation;
326
- }
327
- /** Resolve the reporting child's live direct parent from durable lineage. */
328
- resolveReportParent(child) {
329
- const parentId = child.session.header.parentSession;
330
- /* v8 ignore next -- every continuation-managed child has direct-parent metadata. */
331
- const parent = parentId === undefined ? undefined : this.ctx.agents.get(parentId);
529
+ const parent = this.ctx.agents.get(activation.parentSession);
332
530
  if (parent === undefined) {
333
- throw new SubagentError('direct parent is not live; report was not delivered', 'PARENT_UNAVAILABLE');
334
- }
335
- return parent;
336
- }
337
- /** Deliver one framed report through the selected parent scheduling preset. */
338
- deliverReport(activation, parent, content, delivery) {
339
- const message = createUserMessage({
340
- content: [
341
- { type: 'text', text: `Background subagent ${activation.childId} reported:` },
342
- ...content,
343
- ],
344
- source: {
345
- kind: 'subagent-report',
346
- form: 'relay',
347
- senderSessionId: activation.childId,
348
- },
349
- });
350
- if (delivery === 'wakeup') {
351
- this.sendWaking(parent, message, () => { this.sendReport(parent, message, delivery); });
352
- }
353
- else {
354
- this.sendReport(parent, message, delivery);
531
+ throw new SubagentError('direct parent is not live; the message was not delivered', 'PARENT_UNAVAILABLE');
355
532
  }
533
+ const message = agentMessage(sender, content);
534
+ this.sendWaking(parent, message, () => { this.sendAgentMessage(parent, message); });
356
535
  return message.id;
357
536
  }
358
537
  /**
359
538
  * Perform one waking send to a parent, accounted against that parent's own
360
539
  * Activation when it has one. Registering the id before the send is what
361
540
  * keeps a continuation-managed parent from being judged quiescent in the
362
- * window between `followup()` and the microtask that admits it.
541
+ * window between a waking send and the microtask that admits it.
363
542
  * @param parent - the exact live parent receiving the waking message.
364
543
  * @param message - the message whose id is accounted.
365
544
  * @param send - the synchronous waking send to perform.
@@ -373,16 +552,13 @@ export class SubagentContinuationManager {
373
552
  send();
374
553
  }
375
554
  }
376
- /** Send one report while translating only the parent's own rejection. */
377
- sendReport(parent, message, delivery) {
555
+ /** Send one Agent message while translating only the target's own rejection. */
556
+ sendAgentMessage(parent, message) {
378
557
  try {
379
- if (delivery === 'wakeup')
380
- parent.followup(message);
381
- else
382
- parent.inject(message);
558
+ parent.steer(message);
383
559
  }
384
560
  catch (error) {
385
- throw new SubagentError('direct parent is not live; report was not delivered', 'PARENT_UNAVAILABLE', { cause: error });
561
+ throw new SubagentError('direct parent is not live; the message was not delivered', 'PARENT_UNAVAILABLE', { cause: error });
386
562
  }
387
563
  }
388
564
  /**
@@ -471,6 +647,38 @@ export class SubagentContinuationManager {
471
647
  await Promise.all(materializations.map(materialization => materialization.settled));
472
648
  await this.disposeRoots(targetRoots, 'scoped activation(s)');
473
649
  }
650
+ /**
651
+ * Release selected resident direct children of one exact live parent without
652
+ * closing admission for the parent's other continuable children. Owned
653
+ * descendants are released recursively through the same lifecycle.
654
+ * @param parent - exact live direct parent authorizing the selected release.
655
+ * @param childIds - durable direct-child ids to release when resident.
656
+ * @returns once every selected Activation released its handle.
657
+ * @throws {SubagentError} `UNAUTHORIZED` when a resident target is not the
658
+ * parent's direct continuable child or the parent identity is stale.
659
+ */
660
+ async drainChildren(parent, childIds) {
661
+ if (this.ctx.agents.get(parent.id) !== parent) {
662
+ throw new SubagentError('selected child teardown requires the exact live parent agent', 'UNAUTHORIZED');
663
+ }
664
+ const targets = [];
665
+ for (const childId of new Set(childIds)) {
666
+ const activation = this.activations.get(childId);
667
+ if (activation === undefined)
668
+ continue;
669
+ if (activation.parentSession !== parent.id || !activation.ancestry.has(parent)) {
670
+ throw new SubagentError(`subagent "${childId}" is not a direct child of agent "${parent.id}"`, 'UNAUTHORIZED');
671
+ }
672
+ targets.push(activation);
673
+ }
674
+ // Open every transaction before the first await so cancellation propagates
675
+ // across the selected roots in one synchronous span.
676
+ for (const activation of targets) {
677
+ const disposal = this.dispose(activation);
678
+ void disposal.catch(() => undefined);
679
+ }
680
+ await this.disposeRoots(targets, 'selected activation(s)');
681
+ }
474
682
  /** Dispose independent roots and report every branch failure after all settle. */
475
683
  async disposeRoots(roots, failureSubject) {
476
684
  const failures = await Promise.all(roots.map(async (activation) => {
@@ -559,69 +767,91 @@ export class SubagentContinuationManager {
559
767
  return 'settled';
560
768
  }
561
769
  /**
562
- * Cold-resume a persisted child: inspect and authorize its Session, fold the
770
+ * Cold-resume a persisted child: retain and authorize its prepared Session, fold the
563
771
  * generic descriptor, create the Activation through `ctx.agents.resume()`,
564
772
  * and submit the waiting turn. This never dispatches through a subagent
565
773
  * provider — the persisted Session already holds the initial prefix and the
566
774
  * descriptor is the whole reconstruction input.
567
775
  */
568
776
  async coldResume(parent, childId, content, options) {
569
- const persistence = this.requirePersistence();
570
- let loaded;
777
+ const env_1 = { stack: [], error: void 0, hasError: false };
571
778
  try {
572
- loaded = await persistence.inspect(childId, options.signal);
573
- }
574
- catch (error) {
575
- options.signal.throwIfAborted();
576
- throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
577
- }
578
- options.signal.throwIfAborted();
579
- this.assertAdmitting(parent);
580
- // Authorize the persisted header before folding: only the durable child's
581
- // exact live direct parent may continue it.
582
- this.authorizeLineage(parent, childId, loaded.meta.parentSession);
583
- // Fold only the child's own suffix: a fork seed replays the parent's log,
584
- // which may carry an ANCESTOR's descriptor when the parent is itself a
585
- // continuable child.
586
- const descriptor = foldSubagentDescriptor(loaded.events.slice(loaded.meta.seedLength ?? 0));
587
- if (descriptor === undefined || descriptor.mode !== 'continuable') {
588
- throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed; `
589
- + 'do not retry send_message with this id', 'NOT_RESUMABLE');
779
+ const query = this.requireSessionQuery();
780
+ let observation;
781
+ try {
782
+ observation = await query.observeSession(childId, {
783
+ signal: options.signal,
784
+ });
785
+ }
786
+ catch (error) {
787
+ options.signal.throwIfAborted();
788
+ throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
789
+ }
790
+ const source = __addDisposableResource(env_1, observation, false);
791
+ this.assertAdmitting(parent);
792
+ // Authorize the persisted header before folding: only the durable child's
793
+ // exact live direct parent may continue it.
794
+ this.authorizeLineage(parent, childId, source.header.parentSession);
795
+ // Fold only the child's own suffix: a fork seed replays the parent's log,
796
+ // which may carry an ANCESTOR's descriptor when the parent is itself a
797
+ // continuable child.
798
+ const descriptor = foldSubagentDescriptor(source.events.slice(source.inheritedEventCount));
799
+ if (descriptor === undefined || descriptor.mode !== 'continuable') {
800
+ throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed; choose a different target`, 'NOT_RESUMABLE');
801
+ }
802
+ let activation;
803
+ try {
804
+ activation = await this.materialize({
805
+ childId,
806
+ provider: descriptor.provider,
807
+ parent,
808
+ agentOptions: {
809
+ ...descriptor.agentProvider !== undefined ? { provider: descriptor.agentProvider } : {},
810
+ ...descriptor.agentModel !== undefined ? { model: descriptor.agentModel } : {},
811
+ ...descriptor.agentReasoningEffort !== undefined
812
+ ? { reasoningEffort: ReasoningEffortId(descriptor.agentReasoningEffort) }
813
+ : {},
814
+ },
815
+ composition: { persona: descriptor.persona, toolFilter: descriptor.toolFilter },
816
+ signal: options.signal,
817
+ });
818
+ }
819
+ catch (error) {
820
+ options.signal.throwIfAborted();
821
+ if (error instanceof SubagentError)
822
+ throw error;
823
+ throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
824
+ }
825
+ return await this.submitMaterialized(activation, content, options, parent);
590
826
  }
591
- let activation;
592
- try {
593
- activation = await this.materialize({
594
- childId,
595
- provider: descriptor.provider,
596
- parent,
597
- agentOptions: {
598
- ...descriptor.agentProvider !== undefined ? { provider: descriptor.agentProvider } : {},
599
- ...descriptor.agentModel !== undefined ? { model: descriptor.agentModel } : {},
600
- },
601
- composition: { persona: descriptor.persona, toolFilter: descriptor.toolFilter },
602
- signal: options.signal,
603
- });
827
+ catch (e_1) {
828
+ env_1.error = e_1;
829
+ env_1.hasError = true;
604
830
  }
605
- catch (error) {
606
- options.signal.throwIfAborted();
607
- if (error instanceof SubagentError)
608
- throw error;
609
- throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
831
+ finally {
832
+ __disposeResources(env_1);
610
833
  }
611
- return this.submitMaterialized(activation, content, options.source, parent, options.signal);
612
834
  }
613
835
  /**
614
836
  * Submit to a freshly materialized Activation or roll it back completely.
615
837
  * @param activation - the just-published Activation to admit or release.
616
838
  * @param content - the initial or resumed message content.
617
- * @param source - durable fields naming who supplied the accepted message.
839
+ * @param options - durable source, scheduling, and pre-acceptance cancellation.
618
840
  * @param parent - the live direct parent authorizing admission.
619
- * @param signal - caller cancellation owning admission until acceptance.
620
841
  * @returns the accepted inbox message id.
621
842
  */
622
- async submitMaterialized(activation, content, source, parent, signal) {
843
+ async submitMaterialized(activation, content, options, parent) {
623
844
  try {
624
- return this.submitAdmitted(activation, content, source, parent, signal);
845
+ if (contentHasImage(content)) {
846
+ // The capability read awaits with the activation already published, so
847
+ // the disposal cutoff is re-checked before the submit; a drain that
848
+ // began during the read turns into a clean closing rejection.
849
+ await this.assertImageCapable(activation.handle.agent, options.signal);
850
+ if (activation.disposal !== undefined) {
851
+ throw new SubagentError(`subagent "${activation.childId}" is closing`, 'ACTIVATION_CLOSING');
852
+ }
853
+ }
854
+ return this.submitAdmitted(activation, content, options, parent);
625
855
  }
626
856
  catch (error) {
627
857
  /* v8 ignore next -- rollback disposal failures must not mask the
@@ -630,6 +860,32 @@ export class SubagentContinuationManager {
630
860
  throw error;
631
861
  }
632
862
  }
863
+ /**
864
+ * Refuse image content addressed to a child whose model accepts text only.
865
+ * Callers guard with `contentHasImage`, so text-only delivery never awaits.
866
+ * The check runs inside the per-child delivery lock, before the message
867
+ * exists, so a rejection leaves no partial user message. When the child's
868
+ * route is not fixed by its options (a request-waterfall listener owns it)
869
+ * or no LLM registry is composed, delivery proceeds and the LLM layer's
870
+ * text-only projection replaces each image with its stable placeholder.
871
+ * @param agent - the live or freshly materialized child agent.
872
+ * @param signal - caller cancellation bounding the model-info read.
873
+ * @throws {SubagentError} `MODEL_DOES_NOT_SUPPORT_IMAGES` when the child's resolved model declines image input.
874
+ */
875
+ async assertImageCapable(agent, signal) {
876
+ const { provider, model } = agent.options;
877
+ if (provider === undefined || model === undefined)
878
+ return;
879
+ const llm = this.ctx.get('llm');
880
+ /* v8 ignore next -- a deployment without the LLM registry serves no model
881
+ * to refuse against; delivery then defers to the text-only projection. */
882
+ if (llm === undefined)
883
+ return;
884
+ const info = await llm.resolveModelInfo(provider, model, signal);
885
+ if (info.inputModalities !== undefined && !info.inputModalities.includes('image')) {
886
+ throw new SubagentError(`Model "${model}" does not support image input.`, 'MODEL_DOES_NOT_SUPPORT_IMAGES');
887
+ }
888
+ }
633
889
  /**
634
890
  * Create or resume the child Agent through the private activation-owner
635
891
  * scope, install the handle in a fresh Activation, and register ownership on
@@ -663,14 +919,14 @@ export class SubagentContinuationManager {
663
919
  // some other owner holds — a duplicate would reject there with rollback.
664
920
  inputs.signal.throwIfAborted();
665
921
  const setup = (childCtx) => {
666
- // Only fresh creation seeds the delegation policy onto the child's own
667
- // log (after any fork seed, so fresh policy wins stale seed state); a
668
- // cold resume replays those persisted events instead.
922
+ const child = childCtx.agent;
923
+ // Only fresh creation appends the descriptor and delegated policy after
924
+ // the inherited marker; a cold resume replays those persisted events.
669
925
  if (create !== undefined) {
670
- appendDelegatedPolicyOverrides(childCtx.agent.session, create.delegatedPolicies);
926
+ child.session.append('subagent/descriptor', create.descriptor);
927
+ appendDelegatedPolicyOverrides(child.session, create.delegatedPolicies);
671
928
  }
672
929
  applyChildComposition(childCtx, parent, inputs.composition);
673
- return this.setupRegistry.apply(childCtx);
674
930
  };
675
931
  const observer = this.host.observeActivation(provider, childId, parent);
676
932
  // Agent creation owns rollback before handle transfer. A rejection leaves
@@ -685,7 +941,8 @@ export class SubagentContinuationManager {
685
941
  : await this.ownerCtx.agents.create({
686
942
  sessionId: childId,
687
943
  meta: create.meta,
688
- seed: create.seed,
944
+ ...(create.seed === undefined ? {} : { seed: create.seed }),
945
+ inheritedEventCount: create.inheritedEventCount,
689
946
  agentOptions: inputs.agentOptions,
690
947
  signal: inputs.signal,
691
948
  setup,
@@ -793,13 +1050,18 @@ export class SubagentContinuationManager {
793
1050
  * inbox id. Acceptance is the operation's success boundary; the manager owns
794
1051
  * the Activation independently afterwards.
795
1052
  */
796
- submit(activation, content, source, parent) {
1053
+ submit(activation, content, options, parent) {
797
1054
  // Parent-originated delivery keeps the parent live through ownership, so
798
1055
  // establish it before the message can enter the child's inbox.
799
1056
  this.acquireOwnership(parent, activation.childId);
800
- const message = createUserMessage({ content, source });
1057
+ const message = options.source === undefined
1058
+ ? agentMessage(parent, content)
1059
+ : createUserMessage({ content, source: options.source });
801
1060
  const accepted = this.admitWaking(activation, message.id, () => {
802
- activation.handle.agent.followup(message);
1061
+ if (options.delivery === 'steer')
1062
+ activation.handle.agent.steer(message);
1063
+ else
1064
+ activation.handle.agent.followup(message);
803
1065
  });
804
1066
  // Past this point the caller has an id for this child, so its eventual
805
1067
  // settlement is something the parent is owed an account of.
@@ -814,7 +1076,7 @@ export class SubagentContinuationManager {
814
1076
  * @returns the accepted message id.
815
1077
  */
816
1078
  admitWaking(activation, messageId, send) {
817
- // `Agent.followup()` publishes inbox events synchronously, so observers must
1079
+ // Waking Agent sends publish inbox events synchronously, so observers must
818
1080
  // see this Activation as busy before the call begins.
819
1081
  activation.accepted.add(messageId);
820
1082
  try {
@@ -834,8 +1096,8 @@ export class SubagentContinuationManager {
834
1096
  * manager drain, or Activation disposal that wins before this synchronous
835
1097
  * span rejects without inbox acceptance.
836
1098
  */
837
- submitAdmitted(activation, content, source, parent, signal) {
838
- signal.throwIfAborted();
1099
+ submitAdmitted(activation, content, options, parent) {
1100
+ options.signal.throwIfAborted();
839
1101
  this.assertAdmitting(parent);
840
1102
  /* v8 ignore next 6 -- only a synchronous re-entrant disposer can change
841
1103
  * this field between the caller's live check and this no-await boundary. */
@@ -843,7 +1105,7 @@ export class SubagentContinuationManager {
843
1105
  throw new SubagentError(`subagent "${activation.childId}" activation is being disposed; the message was not accepted`, 'ACTIVATION_CLOSING');
844
1106
  }
845
1107
  this.authorizeLineage(parent, activation.childId, activation.handle.agent.session.header.parentSession);
846
- return this.submit(activation, content, source, parent);
1108
+ return this.submit(activation, content, options, parent);
847
1109
  }
848
1110
  /**
849
1111
  * Authorize one operation against the durable direct-parent lineage. Other
@@ -1095,6 +1357,14 @@ export class SubagentContinuationManager {
1095
1357
  }
1096
1358
  return persistence;
1097
1359
  }
1360
+ /** Resolve the Session query service used for cold child observations. */
1361
+ requireSessionQuery() {
1362
+ const query = this.ctx.get('sessionQuery');
1363
+ if (query === undefined) {
1364
+ throw new SubagentError('continuable subagents require session query (load @xneog/dsh-session-query)', 'CONTINUATION_UNAVAILABLE');
1365
+ }
1366
+ return query;
1367
+ }
1098
1368
  }
1099
1369
  export default SubagentContinuationManager;
1100
1370
  //# sourceMappingURL=continuation.js.map