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