@deepseek-ai/dsh-subagent 0.1.2-alpha.2 → 0.1.2-alpha.4

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.
@@ -27,27 +27,18 @@ import type { SessionId } from '@deepseek-ai/dsh-session';
27
27
  import type { SubagentDescriptorData } from './descriptor.ts';
28
28
  import type { ContinuableCreateRequest, ContinuableCreateSpec, SubagentStartRequest } from './types.ts';
29
29
  import type { ActivationObserver } from './lifecycle.ts';
30
- import type SubagentActivationSetupRegistry from './activation-setup-registry.ts';
31
- /** Attribution for a model coordinator's follow-up to one of its children. */
32
- export interface CoordinatorMessageSource {
33
- readonly kind: 'coordinator';
30
+ /** Durable attribution for one model-authored message between adjacent Agents. */
31
+ export interface AgentMessageSource {
32
+ readonly kind: 'agent-message';
34
33
  /** A message another agent addressed to this one (`relay` context form). */
35
34
  readonly form: 'relay';
36
- /** Session id of the agent whose tool call produced the follow-up. */
37
- readonly senderSessionId: SessionId;
38
- }
39
- /** Durable attribution for a continuable child's explicit parent report. */
40
- export interface SubagentReportMessageSource {
41
- readonly kind: 'subagent-report';
42
- /** A message another agent addressed to this one (`relay` context form). */
43
- readonly form: 'relay';
44
- /** Session id of the reporting child. */
35
+ /** Session id of the Agent whose tool call produced the message. */
45
36
  readonly senderSessionId: SessionId;
46
37
  }
47
38
  /**
48
39
  * Durable attribution for the runtime's own account of a continuable child
49
40
  * settling. Deliberately a different kind from
50
- * {@link SubagentReportMessageSource}: a report is content the child chose,
41
+ * {@link AgentMessageSource}: an Agent message is content the sender chose,
51
42
  * while this message is the manager stating what became of the child, and a
52
43
  * transcript that merged them would credit the child with words it never wrote.
53
44
  */
@@ -62,20 +53,10 @@ export interface SubagentSettledMessageSource {
62
53
  }
63
54
  declare module '@deepseek-ai/dsh-llm' {
64
55
  interface MessageSourceMap {
65
- coordinator: CoordinatorMessageSource;
66
- 'subagent-report': SubagentReportMessageSource;
56
+ 'agent-message': AgentMessageSource;
67
57
  'subagent-settled': SubagentSettledMessageSource;
68
58
  }
69
59
  }
70
- /** Deployment scheduling policy for accepted child reports. */
71
- export type SubagentReportDelivery = 'quiet' | 'next-step';
72
- /** Options for one continuable child's report to its direct parent. */
73
- export interface SubagentReportOptions {
74
- /** Already-resolved parent scheduling policy. */
75
- readonly delivery: SubagentReportDelivery;
76
- /** Caller cancellation, owning authorization and admission until acceptance. */
77
- readonly signal: AbortSignal;
78
- }
79
60
  /** What a caller asks for when starting a continuable background child. */
80
61
  export interface ContinuableStartSpec {
81
62
  /** The `ctx.subagents` provider whose continuable-creation capability establishes the child. */
@@ -115,10 +96,8 @@ export type SubagentInterruptAuthority = {
115
96
  readonly kind: 'ancestor';
116
97
  readonly agent: Agent;
117
98
  };
118
- /** Options for following up with one continuable child. */
119
- export interface SubagentFollowupOptions {
120
- /** Durable attribution retained on the delivered message; it grants no authority. */
121
- readonly source: MessageSource;
99
+ /** Options for one model-authored message between adjacent Agents. */
100
+ export interface SubagentSendMessageOptions {
122
101
  /** Caller cancellation, owning the operation only until inbox acceptance. */
123
102
  readonly signal: AbortSignal;
124
103
  }
@@ -155,7 +134,6 @@ interface ContinuationHost {
155
134
  export declare class SubagentContinuationManager {
156
135
  private readonly ctx;
157
136
  private readonly host;
158
- private readonly setupRegistry;
159
137
  /** Child session id → its live Activation. Process-local, never durable. */
160
138
  private activations;
161
139
  /** Materializations admitted before drain, tracked through publication or rollback. */
@@ -171,7 +149,7 @@ export declare class SubagentContinuationManager {
171
149
  */
172
150
  private readonly closingScopes;
173
151
  private draining;
174
- constructor(ctx: Context, host: ContinuationHost, setupRegistry: SubagentActivationSetupRegistry);
152
+ constructor(ctx: Context, host: ContinuationHost);
175
153
  /**
176
154
  * Start one continuable background child: reserve its durable identity,
177
155
  * resolve the provider's detached creation spec, create the child Agent
@@ -191,23 +169,31 @@ export declare class SubagentContinuationManager {
191
169
  /** Reject one child identity already owned by a live Agent or Session. */
192
170
  private assertChildIdAvailable;
193
171
  /**
194
- * Deliver one later message to a known continuable child as its next FIFO
195
- * turn. Routing depends only on Activation residency: a `running` Activation
196
- * enqueues, a `waiting` one wakes the same Agent, and an absent one
197
- * cold-resumes a new Activation from the persisted Session. The Agent inbox
198
- * is the only queue, so every accepted message has one observable order.
199
- *
200
- * The caller signal owns lookup, materialization, and admission only until
201
- * inbox acceptance; afterwards the accepted turn cannot be cancelled through
202
- * this service.
203
- * @param parent - the exact live direct parent authorizing this delivery.
204
- * @param childId - the durable child session id.
205
- * @param content - the user-role content to deliver.
206
- * @param options - the message source fields and caller cancellation.
172
+ * Deliver one model-authored message to a direct continuable child or to the
173
+ * sender's direct parent. Both directions use Steer: a running target admits
174
+ * the message at its nearest step boundary, while an idle target starts a
175
+ * turn. A missing direct child cold-resumes through the ordinary continuation
176
+ * lifecycle. The caller signal owns the operation only until inbox acceptance.
177
+ * @param sender - exact live Agent authorizing and originating the message.
178
+ * @param targetId - durable direct-parent or direct-child session id.
179
+ * @param content - model-authored content to deliver.
180
+ * @param options - caller cancellation before acceptance.
181
+ * @returns the accepted message's inbox id.
182
+ * @throws when adjacency, availability, or admission rejects delivery.
183
+ */
184
+ sendMessage(sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions): Promise<MessageId>;
185
+ /**
186
+ * Queue one human-authored prompt as a distinct direct-child turn.
187
+ * @param parent - exact live direct parent authorizing delivery.
188
+ * @param childId - durable direct-child session id.
189
+ * @param content - human-authored content to deliver.
190
+ * @param source - durable host-protocol provenance.
191
+ * @param signal - caller cancellation before inbox acceptance.
207
192
  * @returns the accepted message's inbox id.
208
- * @throws when parent authority, availability, or admission rejects the delivery.
209
193
  */
210
- followup(parent: Agent, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions): Promise<MessageId>;
194
+ queuePrompt(parent: Agent, childId: SessionId, content: ContentBlock[], source: MessageSource, signal: AbortSignal): Promise<MessageId>;
195
+ /** Route one parent-originated delivery through residency and cold resume. */
196
+ private deliverToChild;
211
197
  /**
212
198
  * Interrupt one live continuable child's current turn. Admission is
213
199
  * synchronous and the effect is asynchronous: this authorizes the caller,
@@ -230,25 +216,8 @@ export declare class SubagentContinuationManager {
230
216
  * an ancestor outside the target's recorded live lineage.
231
217
  */
232
218
  interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void;
233
- /**
234
- * Deliver explicitly selected content from one resident continuable child to
235
- * its durable direct parent. Sender authorization, parent resolution, and
236
- * send acceptance share one no-await span. Reporting neither concludes the
237
- * child's turn nor changes its Activation lifetime.
238
- * @param child - exact live reporting child; this is the authority credential.
239
- * @param content - selected model-facing content.
240
- * @param options - scheduling policy and pre-acceptance cancellation.
241
- * @returns the stable identity of the message accepted by the parent.
242
- * @throws {SubagentError} when the sender is unauthorized, the parent is not
243
- * live, or continuation admission is closing.
244
- */
245
- reportFrom(child: Agent, content: ContentBlock[], options: SubagentReportOptions): Promise<MessageId>;
246
- /** Authorize only the exact Agent of one resident Activation. */
247
- private authorizeReporter;
248
- /** Resolve the reporting child's live direct parent from durable lineage. */
249
- private resolveReportParent;
250
- /** Deliver one framed report through the selected parent scheduling preset. */
251
- private deliverReport;
219
+ /** Deliver one resident continuable child's message to its live direct parent. */
220
+ private sendToParent;
252
221
  /**
253
222
  * Perform one waking send to a parent, accounted against that parent's own
254
223
  * Activation when it has one. Registering the id before the send is what
@@ -259,8 +228,8 @@ export declare class SubagentContinuationManager {
259
228
  * @param send - the synchronous waking send to perform.
260
229
  */
261
230
  private sendWaking;
262
- /** Send one report while translating only the parent's own rejection. */
263
- private sendReport;
231
+ /** Send one Agent message while translating only the target's own rejection. */
232
+ private sendAgentMessage;
264
233
  /**
265
234
  * Close admission, await every already-admitted materialization through
266
235
  * publication or rollback, then dispose the stable live Activation forest
@@ -334,12 +303,24 @@ export declare class SubagentContinuationManager {
334
303
  * Submit to a freshly materialized Activation or roll it back completely.
335
304
  * @param activation - the just-published Activation to admit or release.
336
305
  * @param content - the initial or resumed message content.
337
- * @param source - durable fields naming who supplied the accepted message.
306
+ * @param options - durable source, scheduling, and pre-acceptance cancellation.
338
307
  * @param parent - the live direct parent authorizing admission.
339
- * @param signal - caller cancellation owning admission until acceptance.
340
308
  * @returns the accepted inbox message id.
341
309
  */
342
310
  private submitMaterialized;
311
+ /**
312
+ * Refuse image content addressed to a child whose model accepts text only.
313
+ * Callers guard with `contentHasImage`, so text-only delivery never awaits.
314
+ * The check runs inside the per-child delivery lock, before the message
315
+ * exists, so a rejection leaves no partial user message. When the child's
316
+ * route is not fixed by its options (a request-waterfall listener owns it)
317
+ * or no LLM registry is composed, delivery proceeds and the LLM layer's
318
+ * text-only projection replaces each image with its stable placeholder.
319
+ * @param agent - the live or freshly materialized child agent.
320
+ * @param signal - caller cancellation bounding the model-info read.
321
+ * @throws {SubagentError} `MODEL_DOES_NOT_SUPPORT_IMAGES` when the child's resolved model declines image input.
322
+ */
323
+ private assertImageCapable;
343
324
  /**
344
325
  * Create or resume the child Agent through the private activation-owner
345
326
  * scope, install the handle in a fresh Activation, and register ownership on
@@ -74,12 +74,14 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
74
74
  });
75
75
  import { randomUUID } from 'node:crypto';
76
76
  import { brandString } from '@deepseek-ai/dsh-brand';
77
- import { ReasoningEffortId, boundContextSummary, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm';
77
+ import { ReasoningEffortId, boundContextSummary, contentHasImage, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm';
78
+ import { SessionLogOffset } from '@deepseek-ai/dsh-session';
78
79
  import { foldSubagentDescriptor, snapshotSubagentDescriptor } from "./descriptor.js";
79
80
  import { appendDelegatedPolicyOverrides, applyChildComposition, captureDelegatedPolicyOverrides, childSessionMeta, resolveChildAgentOptions, resolveChildDepth, } from "./child-agent.js";
80
81
  import { assertSubagentMaxDepth } from "./depth.js";
81
82
  import { seedDescriptorTurn } from "./descriptor-seed.js";
82
83
  import { SubagentError } from "./error.js";
84
+ import { isAdjacentAgentSendMessageTool } from "./internal.js";
83
85
  /**
84
86
  * Read one Activation's current disposal transaction. This indirection exists
85
87
  * because TypeScript would otherwise narrow repeated reads of the mutable field
@@ -90,6 +92,39 @@ import { SubagentError } from "./error.js";
90
92
  function disposalOf(activation) {
91
93
  return activation.disposal;
92
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
+ }
93
128
  /**
94
129
  * One line telling a parent that a background child is finished and why, in
95
130
  * the parent's own task vocabulary.
@@ -151,7 +186,6 @@ class ChildLock {
151
186
  export class SubagentContinuationManager {
152
187
  ctx;
153
188
  host;
154
- setupRegistry;
155
189
  /** Child session id → its live Activation. Process-local, never durable. */
156
190
  activations = new Map();
157
191
  /** Materializations admitted before drain, tracked through publication or rollback. */
@@ -167,10 +201,9 @@ export class SubagentContinuationManager {
167
201
  */
168
202
  closingScopes = new Map();
169
203
  draining = false;
170
- constructor(ctx, host, setupRegistry) {
204
+ constructor(ctx, host) {
171
205
  this.ctx = ctx;
172
206
  this.host = host;
173
- this.setupRegistry = setupRegistry;
174
207
  // Ordinary Cordis owner effects unwind in reverse registration order, which
175
208
  // cannot express the dynamic child graph. Register the private scope's
176
209
  // structural disposer FIRST and the drain SECOND, so reverse unwind invokes
@@ -237,7 +270,7 @@ export class SubagentContinuationManager {
237
270
  });
238
271
  spec.signal.throwIfAborted();
239
272
  this.assertAdmitting(parent);
240
- const lineageSeedLength = prepared.seed?.length ?? 0;
273
+ const inheritedEventCount = SessionLogOffset(prepared.seed?.length ?? 0);
241
274
  const seed = seedDescriptorTurn(childId, prepared.seed, descriptor);
242
275
  const messageId = await this.locks.run(childId, async () => {
243
276
  spec.signal.throwIfAborted();
@@ -256,12 +289,19 @@ export class SubagentContinuationManager {
256
289
  childId,
257
290
  provider: spec.provider,
258
291
  parent,
259
- create: { seed, meta: childSessionMeta(parent, childDepth, lineageSeedLength), delegatedPolicies },
292
+ create: {
293
+ seed,
294
+ meta: childSessionMeta(parent, childDepth, prepared.seed !== undefined),
295
+ inheritedEventCount,
296
+ delegatedPolicies,
297
+ },
260
298
  agentOptions,
261
299
  composition: { persona: request.persona, toolFilter: request.toolFilter },
262
300
  signal: spec.signal,
263
301
  });
264
- return this.submitMaterialized(activation, request.prompt, { kind: 'user' }, parent, spec.signal);
302
+ return this.submitMaterialized(activation, isAdjacentAgentSendMessageTool(this.ctx.get('tools')?.get('send_message', activation.handle.agent))
303
+ ? continuableInitialPrompt(parent.id, request.prompt)
304
+ : request.prompt, { source: { kind: 'user' }, signal: spec.signal, delivery: 'queue' }, parent);
265
305
  });
266
306
  return { childId, messageId };
267
307
  }
@@ -272,23 +312,52 @@ export class SubagentContinuationManager {
272
312
  }
273
313
  }
274
314
  /**
275
- * Deliver one later message to a known continuable child as its next FIFO
276
- * turn. Routing depends only on Activation residency: a `running` Activation
277
- * enqueues, a `waiting` one wakes the same Agent, and an absent one
278
- * cold-resumes a new Activation from the persisted Session. The Agent inbox
279
- * is the only queue, so every accepted message has one observable order.
280
- *
281
- * The caller signal owns lookup, materialization, and admission only until
282
- * inbox acceptance; afterwards the accepted turn cannot be cancelled through
283
- * this service.
284
- * @param parent - the exact live direct parent authorizing this delivery.
285
- * @param childId - the durable child session id.
286
- * @param content - the user-role content to deliver.
287
- * @param options - the message source fields and caller cancellation.
315
+ * Deliver one model-authored message to a direct continuable child or to the
316
+ * sender's direct parent. Both directions use Steer: a running target admits
317
+ * the message at its nearest step boundary, while an idle target starts a
318
+ * turn. A missing direct child cold-resumes through the ordinary continuation
319
+ * lifecycle. The caller signal owns the operation only until inbox acceptance.
320
+ * @param sender - exact live Agent authorizing and originating the message.
321
+ * @param targetId - durable direct-parent or direct-child session id.
322
+ * @param content - model-authored content to deliver.
323
+ * @param options - caller cancellation before acceptance.
288
324
  * @returns the accepted message's inbox id.
289
- * @throws when parent authority, availability, or admission rejects the delivery.
325
+ * @throws when adjacency, availability, or admission rejects delivery.
290
326
  */
291
- async followup(parent, childId, content, options) {
327
+ async sendMessage(sender, targetId, content, options) {
328
+ if (this.ctx.agents.get(sender.id) !== sender) {
329
+ throw new SubagentError('message delivery requires the exact live sender agent', 'UNAUTHORIZED');
330
+ }
331
+ this.assertAdmitting(sender);
332
+ const senderActivation = this.activations.get(sender.id);
333
+ if (senderActivation !== undefined
334
+ && senderActivation.handle.agent === sender
335
+ && senderActivation.parentSession === targetId) {
336
+ options.signal.throwIfAborted();
337
+ return this.sendToParent(senderActivation, sender, content);
338
+ }
339
+ if (sender.session.header.parentSession === targetId) {
340
+ throw new SubagentError(`agent "${sender.id}" is not a resident continuable child and cannot send to parent "${targetId}"`, 'UNAUTHORIZED');
341
+ }
342
+ return this.deliverToChild(sender, targetId, content, {
343
+ signal: options.signal,
344
+ delivery: 'steer',
345
+ });
346
+ }
347
+ /**
348
+ * Queue one human-authored prompt as a distinct direct-child turn.
349
+ * @param parent - exact live direct parent authorizing delivery.
350
+ * @param childId - durable direct-child session id.
351
+ * @param content - human-authored content to deliver.
352
+ * @param source - durable host-protocol provenance.
353
+ * @param signal - caller cancellation before inbox acceptance.
354
+ * @returns the accepted message's inbox id.
355
+ */
356
+ async queuePrompt(parent, childId, content, source, signal) {
357
+ return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'queue' });
358
+ }
359
+ /** Route one parent-originated delivery through residency and cold resume. */
360
+ async deliverToChild(parent, childId, content, options) {
292
361
  this.assertAdmitting(parent);
293
362
  while (true) {
294
363
  const live = await this.locks.run(childId, async () => {
@@ -297,14 +366,27 @@ export class SubagentContinuationManager {
297
366
  return this.coldResume(parent, childId, content, options);
298
367
  // A delivery that arrives after the disposal transaction began must not
299
368
  // reach a handle being torn down; wait for release, then cold-resume.
369
+ const disposal = activation.disposal;
300
370
  /* v8 ignore next 3 -- the send-versus-dispose cutoff: reaching this arm needs a
301
371
  * delivery to observe the transaction inside the same critical section that opened it,
302
372
  * which no test can schedule deterministically. The behavior is covered end-to-end by
303
373
  * "cold-resumes a delivery that lost the race with final disposal". */
304
- if (activation.disposal !== undefined) {
305
- return activation.disposal.then(() => undefined, () => undefined);
374
+ if (disposal !== undefined) {
375
+ return disposal.then(() => undefined, () => undefined);
306
376
  }
307
- return this.submitAdmitted(activation, content, options.source, parent, options.signal);
377
+ // Text-only delivery stays await-free, so the disposal-cutoff check
378
+ // above and the submit share one critical window. The image path
379
+ // awaits a capability read, so it re-checks the cutoff afterwards; a
380
+ // disposal that began during the read is waited out and retried like
381
+ // one observed on entry.
382
+ if (contentHasImage(content)) {
383
+ await this.assertImageCapable(activation.handle.agent, options.signal);
384
+ if (activation.disposal !== undefined) {
385
+ await Promise.allSettled([activation.disposal]);
386
+ return undefined;
387
+ }
388
+ }
389
+ return this.submitAdmitted(activation, content, options, parent);
308
390
  });
309
391
  /* v8 ignore start -- only the lost-cutoff arm above returns undefined, so only that
310
392
  * race reaches the retry below, which then cold-resumes a new Activation. */
@@ -365,68 +447,19 @@ export class SubagentContinuationManager {
365
447
  return;
366
448
  activation.handle.agent.cancel(authority.kind === 'user' ? { kind: 'user' } : { kind: 'parent' }, { keepInbox: true });
367
449
  }
368
- /**
369
- * Deliver explicitly selected content from one resident continuable child to
370
- * its durable direct parent. Sender authorization, parent resolution, and
371
- * send acceptance share one no-await span. Reporting neither concludes the
372
- * child's turn nor changes its Activation lifetime.
373
- * @param child - exact live reporting child; this is the authority credential.
374
- * @param content - selected model-facing content.
375
- * @param options - scheduling policy and pre-acceptance cancellation.
376
- * @returns the stable identity of the message accepted by the parent.
377
- * @throws {SubagentError} when the sender is unauthorized, the parent is not
378
- * live, or continuation admission is closing.
379
- */
380
- // oxlint-disable-next-line typescript/require-await -- keep rejection semantics without yielding during admission
381
- async reportFrom(child, content, options) {
382
- options.signal.throwIfAborted();
383
- this.assertAdmitting(child);
384
- const activation = this.authorizeReporter(child);
385
- const parent = this.resolveReportParent(child);
386
- return this.deliverReport(activation, parent, content, options.delivery);
387
- }
388
- /** Authorize only the exact Agent of one resident Activation. */
389
- authorizeReporter(child) {
390
- const activation = this.activations.get(child.id);
391
- if (activation === undefined || activation.handle.agent !== child) {
392
- throw new SubagentError(`agent "${child.id}" is not a live continuable subagent and cannot report`, 'UNAUTHORIZED');
393
- }
394
- /* v8 ignore next 6 -- only a synchronous re-entrant disposer can open this
395
- * transaction between exact-agent authorization and this no-await cutoff. */
450
+ /** Deliver one resident continuable child's message to its live direct parent. */
451
+ sendToParent(activation, sender, content) {
452
+ /* v8 ignore next 6 -- only synchronous re-entrant teardown can open this
453
+ * transaction between exact-agent authorization and this no-await span. */
396
454
  if (activation.disposal !== undefined) {
397
- throw new SubagentError(`subagent "${child.id}" activation is being disposed; the report was not delivered`, 'ACTIVATION_CLOSING');
455
+ throw new SubagentError(`subagent "${sender.id}" activation is being disposed; the message was not delivered`, 'ACTIVATION_CLOSING');
398
456
  }
399
- return activation;
400
- }
401
- /** Resolve the reporting child's live direct parent from durable lineage. */
402
- resolveReportParent(child) {
403
- const parentId = child.session.header.parentSession;
404
- /* v8 ignore next -- every continuation-managed child has direct-parent metadata. */
405
- const parent = parentId === undefined ? undefined : this.ctx.agents.get(parentId);
457
+ const parent = this.ctx.agents.get(activation.parentSession);
406
458
  if (parent === undefined) {
407
- throw new SubagentError('direct parent is not live; report was not delivered', 'PARENT_UNAVAILABLE');
408
- }
409
- return parent;
410
- }
411
- /** Deliver one framed report through the selected parent scheduling preset. */
412
- deliverReport(activation, parent, content, delivery) {
413
- const message = createUserMessage({
414
- content: [
415
- { type: 'text', text: `Background subagent ${activation.childId} reported:` },
416
- ...content,
417
- ],
418
- source: {
419
- kind: 'subagent-report',
420
- form: 'relay',
421
- senderSessionId: activation.childId,
422
- },
423
- });
424
- if (delivery === 'next-step') {
425
- this.sendWaking(parent, message, () => { this.sendReport(parent, message, delivery); });
426
- }
427
- else {
428
- this.sendReport(parent, message, delivery);
459
+ throw new SubagentError('direct parent is not live; the message was not delivered', 'PARENT_UNAVAILABLE');
429
460
  }
461
+ const message = agentMessage(sender, content);
462
+ this.sendWaking(parent, message, () => { this.sendAgentMessage(parent, message); });
430
463
  return message.id;
431
464
  }
432
465
  /**
@@ -447,16 +480,13 @@ export class SubagentContinuationManager {
447
480
  send();
448
481
  }
449
482
  }
450
- /** Send one report while translating only the parent's own rejection. */
451
- sendReport(parent, message, delivery) {
483
+ /** Send one Agent message while translating only the target's own rejection. */
484
+ sendAgentMessage(parent, message) {
452
485
  try {
453
- if (delivery === 'next-step')
454
- parent.steer(message);
455
- else
456
- parent.inject(message);
486
+ parent.steer(message);
457
487
  }
458
488
  catch (error) {
459
- throw new SubagentError('direct parent is not live; report was not delivered', 'PARENT_UNAVAILABLE', { cause: error });
489
+ throw new SubagentError('direct parent is not live; the message was not delivered', 'PARENT_UNAVAILABLE', { cause: error });
460
490
  }
461
491
  }
462
492
  /**
@@ -693,10 +723,9 @@ export class SubagentContinuationManager {
693
723
  // Fold only the child's own suffix: a fork seed replays the parent's log,
694
724
  // which may carry an ANCESTOR's descriptor when the parent is itself a
695
725
  // continuable child.
696
- const descriptor = foldSubagentDescriptor(source.events.slice(source.header.seedLength ?? 0));
726
+ const descriptor = foldSubagentDescriptor(source.events.slice(source.inheritedEventCount));
697
727
  if (descriptor === undefined || descriptor.mode !== 'continuable') {
698
- throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed; `
699
- + 'do not retry send_message with this id', 'NOT_RESUMABLE');
728
+ throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed; choose a different target`, 'NOT_RESUMABLE');
700
729
  }
701
730
  let activation;
702
731
  try {
@@ -721,7 +750,7 @@ export class SubagentContinuationManager {
721
750
  throw error;
722
751
  throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
723
752
  }
724
- return await this.submitMaterialized(activation, content, options.source, parent, options.signal);
753
+ return await this.submitMaterialized(activation, content, options, parent);
725
754
  }
726
755
  catch (e_1) {
727
756
  env_1.error = e_1;
@@ -735,14 +764,22 @@ export class SubagentContinuationManager {
735
764
  * Submit to a freshly materialized Activation or roll it back completely.
736
765
  * @param activation - the just-published Activation to admit or release.
737
766
  * @param content - the initial or resumed message content.
738
- * @param source - durable fields naming who supplied the accepted message.
767
+ * @param options - durable source, scheduling, and pre-acceptance cancellation.
739
768
  * @param parent - the live direct parent authorizing admission.
740
- * @param signal - caller cancellation owning admission until acceptance.
741
769
  * @returns the accepted inbox message id.
742
770
  */
743
- async submitMaterialized(activation, content, source, parent, signal) {
771
+ async submitMaterialized(activation, content, options, parent) {
744
772
  try {
745
- return this.submitAdmitted(activation, content, source, parent, signal);
773
+ if (contentHasImage(content)) {
774
+ // The capability read awaits with the activation already published, so
775
+ // the disposal cutoff is re-checked before the submit; a drain that
776
+ // began during the read turns into a clean closing rejection.
777
+ await this.assertImageCapable(activation.handle.agent, options.signal);
778
+ if (activation.disposal !== undefined) {
779
+ throw new SubagentError(`subagent "${activation.childId}" is closing`, 'ACTIVATION_CLOSING');
780
+ }
781
+ }
782
+ return this.submitAdmitted(activation, content, options, parent);
746
783
  }
747
784
  catch (error) {
748
785
  /* v8 ignore next -- rollback disposal failures must not mask the
@@ -751,6 +788,32 @@ export class SubagentContinuationManager {
751
788
  throw error;
752
789
  }
753
790
  }
791
+ /**
792
+ * Refuse image content addressed to a child whose model accepts text only.
793
+ * Callers guard with `contentHasImage`, so text-only delivery never awaits.
794
+ * The check runs inside the per-child delivery lock, before the message
795
+ * exists, so a rejection leaves no partial user message. When the child's
796
+ * route is not fixed by its options (a request-waterfall listener owns it)
797
+ * or no LLM registry is composed, delivery proceeds and the LLM layer's
798
+ * text-only projection replaces each image with its stable placeholder.
799
+ * @param agent - the live or freshly materialized child agent.
800
+ * @param signal - caller cancellation bounding the model-info read.
801
+ * @throws {SubagentError} `MODEL_DOES_NOT_SUPPORT_IMAGES` when the child's resolved model declines image input.
802
+ */
803
+ async assertImageCapable(agent, signal) {
804
+ const { provider, model } = agent.options;
805
+ if (provider === undefined || model === undefined)
806
+ return;
807
+ const llm = this.ctx.get('llm');
808
+ /* v8 ignore next -- a deployment without the LLM registry serves no model
809
+ * to refuse against; delivery then defers to the text-only projection. */
810
+ if (llm === undefined)
811
+ return;
812
+ const info = await llm.resolveModelInfo(provider, model, signal);
813
+ if (info.inputModalities !== undefined && !info.inputModalities.includes('image')) {
814
+ throw new SubagentError(`Model "${model}" does not support image input.`, 'MODEL_DOES_NOT_SUPPORT_IMAGES');
815
+ }
816
+ }
754
817
  /**
755
818
  * Create or resume the child Agent through the private activation-owner
756
819
  * scope, install the handle in a fresh Activation, and register ownership on
@@ -791,7 +854,6 @@ export class SubagentContinuationManager {
791
854
  appendDelegatedPolicyOverrides(childCtx.agent.session, create.delegatedPolicies);
792
855
  }
793
856
  applyChildComposition(childCtx, parent, inputs.composition);
794
- return this.setupRegistry.apply(childCtx);
795
857
  };
796
858
  const observer = this.host.observeActivation(provider, childId, parent);
797
859
  // Agent creation owns rollback before handle transfer. A rejection leaves
@@ -807,6 +869,7 @@ export class SubagentContinuationManager {
807
869
  sessionId: childId,
808
870
  meta: create.meta,
809
871
  seed: create.seed,
872
+ inheritedEventCount: create.inheritedEventCount,
810
873
  agentOptions: inputs.agentOptions,
811
874
  signal: inputs.signal,
812
875
  setup,
@@ -914,13 +977,18 @@ export class SubagentContinuationManager {
914
977
  * inbox id. Acceptance is the operation's success boundary; the manager owns
915
978
  * the Activation independently afterwards.
916
979
  */
917
- submit(activation, content, source, parent) {
980
+ submit(activation, content, options, parent) {
918
981
  // Parent-originated delivery keeps the parent live through ownership, so
919
982
  // establish it before the message can enter the child's inbox.
920
983
  this.acquireOwnership(parent, activation.childId);
921
- const message = createUserMessage({ content, source });
984
+ const message = options.delivery === 'steer'
985
+ ? agentMessage(parent, content)
986
+ : createUserMessage({ content, source: options.source });
922
987
  const accepted = this.admitWaking(activation, message.id, () => {
923
- activation.handle.agent.followup(message);
988
+ if (options.delivery === 'steer')
989
+ activation.handle.agent.steer(message);
990
+ else
991
+ activation.handle.agent.followup(message);
924
992
  });
925
993
  // Past this point the caller has an id for this child, so its eventual
926
994
  // settlement is something the parent is owed an account of.
@@ -955,8 +1023,8 @@ export class SubagentContinuationManager {
955
1023
  * manager drain, or Activation disposal that wins before this synchronous
956
1024
  * span rejects without inbox acceptance.
957
1025
  */
958
- submitAdmitted(activation, content, source, parent, signal) {
959
- signal.throwIfAborted();
1026
+ submitAdmitted(activation, content, options, parent) {
1027
+ options.signal.throwIfAborted();
960
1028
  this.assertAdmitting(parent);
961
1029
  /* v8 ignore next 6 -- only a synchronous re-entrant disposer can change
962
1030
  * this field between the caller's live check and this no-await boundary. */
@@ -964,7 +1032,7 @@ export class SubagentContinuationManager {
964
1032
  throw new SubagentError(`subagent "${activation.childId}" activation is being disposed; the message was not accepted`, 'ACTIVATION_CLOSING');
965
1033
  }
966
1034
  this.authorizeLineage(parent, activation.childId, activation.handle.agent.session.header.parentSession);
967
- return this.submit(activation, content, source, parent);
1035
+ return this.submit(activation, content, options, parent);
968
1036
  }
969
1037
  /**
970
1038
  * Authorize one operation against the durable direct-parent lineage. Other