返回源码地图

packages/subagent/subagent/src/index.ts

main snapshot · da00f7f5358f · 正文引用章节 13 / 13 / 13;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/**
2 * Service Definition for the subagent capability seam (`ctx.subagents`): a named-provider registry plus a
3 * capability-validating asynchronous start API. Providers establish a
4 * child before returning its run, so fulfillment is the single publication and
5 * ownership-transfer boundary.
6 *
7 * Multiple providers coexist: each registers under a unique name and callers
8 * select one by name.
9 *
10 * This package owns the Service Definition role of the capability seam. Service Providers
11 * (`@deepseek-ai/dsh-subagent-spawn-in-process`, `-fork`, `-acp`) and the model-facing
12 * consumer (`@deepseek-ai/dsh-tool-subagent`) are separate packages.
13 *
14 * Public operations express caller intent: `start` returns one published owned
15 * one-shot run, `startContinuable` establishes a durable continuable child, and
16 * `sendMessage` steers between adjacent Agents without exposing whether a child
17 * is resident. Continuable children never become a {@link SubagentRun}: the
18 * continuation manager holds their `AgentHandle` directly and orders every turn
19 * through the child's own inbox, so providers contribute only the detached
20 * creation spec and see no handle, turn, or teardown. Direct-child
21 * discovery reads the parent catalog; descendant discovery recursively reads
22 * those child catalogs. Neither read requires the continuation runtime.
23 *
24 * Same-process providers are trusted typed collaborators. Requests, provider
25 * descriptors, results, and lifecycle payloads are borrowed immutable values;
26 * serialization and hostile-input validation belong at real process, worker,
27 * persistence, and model boundaries.
28 *
29 * @module @deepseek-ai/dsh-subagent
30 */
31import type { Volatile } from '@deepseek-ai/cordis'
32
33import { Context } from '@deepseek-ai/cordis'
34import z from '@deepseek-ai/schemastery'
35import type {} from '@deepseek-ai/dsh-attachment'
36import { scopeTarget } from '@deepseek-ai/dsh-scope'
37import type { Scoped } from '@deepseek-ai/dsh-scope'
38import { assertObjectJsonSchema } from '@deepseek-ai/dsh-tools'
39import type { ContentBlock, MessageId, MessageSource } from '@deepseek-ai/dsh-llm'
40import type { Agent } from '@deepseek-ai/dsh-agent'
41import type { SessionId } from '@deepseek-ai/dsh-session'
42import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time'
43import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
44import {
45 rejectPrompt, validateControlRequest,
46} from './control.ts'
47import type {
48 SubagentInterruptReceipt,
49 SubagentPromptReceipt,
50 SubagentPromptRequest,
51 SubagentPromptRequestId,
52} from './control-types.ts'
53import type {
54 ContinuableCreateRequest,
55 ContinuableCreateSpec,
56 ContinuableStart,
57 ContinuableStartSpec,
58 ResolvedSubagentStartRequest,
59 SubagentCapabilities,
60 SubagentInterruptAuthority,
61 SubagentProvider,
62 SubagentRun,
63 SubagentRunEndInfo,
64 SubagentRunInfo,
65 SubagentSendMessageOptions,
66 SubagentStartRequest,
67} from './types.ts'
68import { SubagentError } from './error.ts'
69import { assertSubagentMaxDepth } from './depth.ts'
70import { createActivationObserver, createLifecycleEmitter, observeRun } from './lifecycle.ts'
71import type { ActivationObserver, LifecycleEmitter } from './lifecycle.ts'
72import SubagentContinuationManager from './continuation.ts'
73import type { SubagentDelivery } from './inbox.ts'
74import { listChildren as listSubagentChildren, listDescendants as listSubagentDescendants } from './list-children.ts'
75import type { SubagentDescendantListEntry } from './list-children.ts'
76import { installSubagentArchiveAdmission } from './archive-admission.ts'
77import { snapshotSubagentDescriptor } from './descriptor.ts'
78import { subagentIdentityProjectionDefinition, subagentTimingProjectionDefinition } from './projection.ts'
79import { establishCatalogChild, subagentCatalogProjectionDefinition } from './catalog.ts'
80import type { SubagentCatalogEntry } from './projection-types.ts'
81import { deliverSubagentPrompt } from './internal.ts'
82
83export type {} from './catalog.ts'
84export * from './out-of-process.ts'
85export { AssistantOutputFold, finalAssistantOutput } from './assistant-output.ts'
86export { SubagentRunId } from './types.ts'
87export type {
88 ContinuableCreateRequest,
89 ContinuableCreateSpec,
90 ContinuableStart,
91 ContinuableStartSpec,
92 ResolvedSubagentStartRequest,
93 SubagentCapabilities,
94 SubagentInterruptAuthority,
95 SubagentProvider,
96 SubagentResult,
97 SubagentRun,
98 SubagentSendMessageOptions,
99 SubagentStartRequest,
100 SubagentStopReason,
101 SubagentStopReasonMap,
102} from './types.ts'
103export {
104 foldSubagentDescriptor,
105 snapshotSubagentDescriptor,
106 SUBAGENT_DESCRIPTOR_VERSION,
107} from './descriptor.ts'
108export type {
109 ContinuableSubagentDescriptorData,
110 ContinuableSubagentDescriptorInput,
111 OneShotSubagentDescriptorData,
112 OneShotSubagentDescriptorInput,
113 SubagentDescriptorData,
114 SubagentDescriptorInput,
115} from './descriptor.ts'
116export type { SubagentCatalogEntry } from './projection-types.ts'
117export { SubagentError } from './error.ts'
118export { settleRun } from './run-settlement.ts'
119export { assertSubagentMaxDepth, delegationDepthOf } from './depth.ts'
120export {
121 appendDelegatedPolicyOverrides,
122 applyChildComposition,
123 captureDelegatedPolicyOverrides,
124 childSessionMeta,
125 parentAgentOptionsForDelegation,
126 resolveChildAgentOptions,
127 resolveChildDepth,
128 SubagentDepthError,
129} from './child-agent.ts'
130export type { ChildComposition, DelegatedPolicyOverrides } from './child-agent.ts'
131export type { AgentMessageSource, SubagentSettledMessageSource } from './continuation-messages.ts'
132export type * from './control-types.ts'
133export type { SubagentDescendantListEntry } from './list-children.ts'
134export type { SubagentRunEndInfo, SubagentRunInfo } from './types.ts'
135export type { SubagentIdentityProjection, SubagentTimingProjection } from './projection-types.ts'
136
137declare module '@deepseek-ai/cordis' {
138 interface Context {
139 subagents: SubagentRuntime
140 }
141
142 interface Events {
143 /**
144 * A provider became resolvable in the registry.
145 * @param provider - the registered provider.
146 * @mode emit
147 */
148 'subagent/provider-added'(provider: SubagentProvider): void
149 /**
150 * A provider left the registry. Accepted runs remain holder-owned.
151 * @param name - the provider name that no longer resolves.
152 * @mode emit
153 */
154 'subagent/provider-removed'(name: string): void
155 /**
156 * A provider established a published child. For in-process providers,
157 * `ctx.agents.get(info.id)` resolves during this notification.
158 * Scope-filtered dispatch keys the carrier by the delegating parent, so a
159 * parent-scoped listener observes only its own delegations. Paired with
160 * `subagent/end`.
161 * @param info - the provider and published child identity.
162 * @mode emit
163 */
164 'subagent/start'(this: Scoped<SubagentRuntime>, info: SubagentRunInfo): void
165 /**
166 * A published child settled. Scope-filtered dispatch uses the same delegating
167 * parent carrier as `subagent/start`, so the lifecycle pair reaches the
168 * same scoped audience.
169 * @param info - the run identity and terminal outcome.
170 * @mode emit
171 */
172 'subagent/end'(this: Scoped<SubagentRuntime>, info: SubagentRunEndInfo): void
173 }
174}
175
176/**
177 * Durable attribution of one browser-authored follow-up. The Session
178 * Controller declares this `user-rpc` message source and depends on this
179 * package, so the fields are spelled here: `MessageSource`'s `user` member
180 * accepts the record and the correlation id rides the durable message the
181 * Client reconciles its optimistic prompt against.
182 */
183interface BrowserPromptSource {
184 readonly kind: 'user'
185 readonly rpcId: SubagentPromptRequestId
186 readonly clientTimeZone?: string
187}
188
189/** Host configuration for continuable subagent capacity. */
190export interface Config {
191 /** Maximum live children sharing uninterrupted continuable parent links; defaults to 8. */
192 maxActiveSubagents: Volatile<number>
193 /** Default delegation depth for tools without an explicit limit; defaults to 1. */
194 maxDepth: Volatile<number>
195}
196
197/** Named provider registry with one-shot runs, durable discovery, and continuable-child operations. */
198export class SubagentRuntime extends TypertRemoteService {
199 static Config = z.object({
200 maxDepth: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(1).volatile(),
201 maxActiveSubagents: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(8).volatile(),
202 })
203 private providers = new Map<string, SubagentProvider>()
204 private continuations: SubagentContinuationManager | undefined
205 /**
206 * The contained lifecycle-edge publisher. Built here because scoped dispatch
207 * keys its carrier by this exact service instance, whose own context filter
208 * composes into the carrier.
209 */
210 private readonly emitLifecycle: LifecycleEmitter
211
212 constructor(ctx: Context, private config: Config) {
213 super(ctx, 'subagents')
214 this.emitLifecycle = createLifecycleEmitter(this.ctx, parent => scopeTarget(this, parent))
215 ctx.inject(['agents'], (childCtx: Context) => {
216 const manager = new SubagentContinuationManager(childCtx, {
217 prepareContinuable: (name, request) => this.prepareContinuable(name, request),
218 observeActivation: (provider, childId, parent) => this.observeActivation(provider, childId, parent),
219 }, () => this.config.maxActiveSubagents.get())
220 this.continuations = manager
221 childCtx.effect(() => () => {
222 /* v8 ignore else -- one injected binding owns the slot until its fiber disposes. */
223 if (this.continuations === manager) this.continuations = undefined
224 }, 'subagents.continuationBinding()')
225 })
226 ctx.inject(['sessionProjections'], (projectionCtx) => {
227 const projections = projectionCtx.sessionProjections
228 projections.register(subagentCatalogProjectionDefinition)
229 projections.register(subagentTimingProjectionDefinition)
230 projections.register(subagentIdentityProjectionDefinition)
231 })
232 // Archive admission: this runtime is the owner that knows which live
233 // children descend from a Session and how a parent stops them.
234 ctx.inject(['agents'], (agentsCtx: Context) => { installSubagentArchiveAdmission(agentsCtx) })
235 }
236
237 /**
238 * Resolve a delegation tool's depth policy against the current user setting.
239 * @param configured - Explicit tool limit, or provider-managed for external delegation.
240 * @returns The numeric limit, or undefined when the provider owns depth enforcement.
241 */
242 resolveMaxDepth(configured?: number | 'provider-managed'): number | undefined {
243 if (configured === 'provider-managed') return undefined
244 if (configured !== undefined) return configured
245 const depth = this.config.maxDepth.get()
246 assertSubagentMaxDepth(depth)
247 return depth
248 }
249
250 /**
251 * Establish one durable continuable child and deliver its initial prompt.
252 * Resolves when the child's inbox accepts that prompt, without waiting for the
253 * turn to start or for the message to reach the Session log; any earlier
254 * failure rejects with no ids and rolls back the child entirely.
255 * @param spec - provider, delegation request, and caller cancellation.
256 * @returns the durable child id and the accepted prompt's message id.
257 * @throws when continuation services are unavailable or materialization fails.
258 */
259 async startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart> {
260 return this.requireContinuations().startContinuable(spec)
261 }
262
263 /**
264 * Steer one model-authored message to the sender's direct parent or direct
265 * continuable child. A running target admits it at the nearest step boundary;
266 * an idle target starts a turn, and an absent direct child cold-resumes from
267 * persistence. The service derives durable sender attribution from the exact
268 * live sender. Caller cancellation stops only pre-acceptance work.
269 * @param sender - exact live Agent authorizing and originating the message.
270 * @param targetId - durable direct-parent or direct-child session id.
271 * @param content - model-authored content to deliver.
272 * @param options - caller cancellation before inbox acceptance.
273 * @returns the accepted message's inbox id.
274 * @throws when continuation services are unavailable, adjacency is rejected,
275 * or the message was not admitted.
276 */
277 async sendMessage(
278 sender: Agent,
279 targetId: SessionId,
280 content: ContentBlock[],
281 options: SubagentSendMessageOptions,
282 ): Promise<MessageId> {
283 return this.requireContinuations().sendMessage(sender, targetId, content, options)
284 }
285
286 /**
287 * Deliver one host-protocol message to a direct continuable child.
288 * Symbol-keyed so host adapters can preserve their own source descriptors without
289 * widening the public Service Definition or impersonating an Agent sender.
290 * @param parent - exact live direct parent authorizing delivery.
291 * @param childId - durable direct-child session id.
292 * @param content - host-authored content to deliver.
293 * @param source - durable host-protocol source descriptor.
294 * @param signal - caller cancellation before inbox acceptance.
295 * @param delivery - Queue as a distinct turn or Steer at the nearest step.
296 * @returns the accepted message's inbox id.
297 */
298 private [deliverSubagentPrompt](
299 parent: Agent,
300 childId: SessionId,
301 content: ContentBlock[],
302 source: MessageSource,
303 signal: AbortSignal,
304 delivery: SubagentDelivery,
305 ): Promise<MessageId> {
306 return delivery === 'steer'
307 ? this.requireContinuations().steerPrompt(parent, childId, content, source, signal)
308 : this.requireContinuations().queuePrompt(parent, childId, content, source, signal)
309 }
310
311 /**
312 * Interrupt one live continuable child's current turn under a human parent
313 * address or an exact live ancestor Agent. Fire-and-return: the cancel
314 * signal is issued before this returns, but the target may keep running
315 * until it observes the signal. Unclaimed pending inbox work, the Activation,
316 * and published descendants are preserved; claimed work is not requeued.
317 * Once the interrupted driver is idle, a waking send resumes the parked FIFO
318 * queue. An absent target — including a one-shot or unknown id —
319 * is an accepted no-op, as is a manager-less composition, which cannot own a
320 * live Activation.
321 * @param targetSessionId - the durable child session id to interrupt.
322 * @param authority - the human parent address or exact live ancestor Agent.
323 * @throws {SubagentError} `UNAUTHORIZED` when the authority does not own the
324 * live target.
325 */
326 interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void {
327 this.continuations?.interrupt(targetSessionId, authority)
328 }
329
330 /**
331 * Close continuable admission below exact live parent Agents, stop only their
332 * visible descendant Activations synchronously, then await admitted scoped
333 * materializations and release those forests child-first. The scoped cutoff
334 * lasts until each exact parent leaves the registry; unrelated parent trees
335 * remain live.
336 * @param parents - exact host-owned parent Agents entering teardown.
337 * @returns once every retained descendant Activation released its `AgentHandle`.
338 * @throws an aggregate error after all branches settle when any failed.
339 */
340 async drainContinuableDescendants(parents: readonly Agent[]): Promise<void> {
341 const manager = this.continuations
342 // Absent continuation services means nothing was ever materialized.
343 if (manager === undefined) return
344 await manager.drainDescendants(parents)
345 }
346
347 /**
348 * Release selected resident continuable direct children of one exact live
349 * parent. Other children of the same parent remain admitted and resident.
350 * Absent targets and a manager-less composition are accepted no-ops.
351 * @param parent - exact live direct parent authorizing the selected release.
352 * @param childIds - durable direct-child ids to release when resident.
353 * @returns once every selected Activation released its `AgentHandle`.
354 * @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a
355 * different parent or the supplied parent identity is stale.
356 */
357 async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void> {
358 const manager = this.continuations
359 if (manager === undefined) return
360 await manager.drainChildren(parent, childIds)
361 }
362
363 /**
364 * Read the parent's durable direct-child catalog without loading or resuming an Agent.
365 * The service owns and releases the live-preferred Session observation.
366 * @param parentSessionId - parent whose direct children are requested.
367 * @param signal - cancellation forwarded to the Session query.
368 * @returns catalog children in parent event order.
369 * @throws {@link SubagentError} when query or catalog projection is unavailable.
370 * @throws SessionQueryError when the parent cannot be read or the query is cancelled.
371 */
372 listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise<SubagentCatalogEntry[]> {
373 return listSubagentChildren(this.ctx, parentSessionId, signal)
374 }
375
376 /**
377 * Recursively list reachable parent catalogs in stable pre-order, preserving
378 * each catalog's event order. Each row carries its catalog parent and depth;
379 * one-shot and unknown-mode children remain traversal nodes. Unknown modes
380 * produce unsupported diagnostics. Unreadable child catalogs produce corrupt
381 * or unavailable diagnostics and stop only that branch. Root read failures,
382 * missing services or projections, and cancellation reject the whole listing.
383 * Each catalog is observed once and released before the next read. No Agent
384 * is loaded or resumed; Sessions absent from reachable catalogs are omitted.
385 * @param rootSessionId - session whose catalog starts descendant discovery.
386 * @param signal - cancellation forwarded to and checked around each catalog read.
387 * @returns children and branch diagnostics in parent-catalog pre-order.
388 * @throws {@link SubagentError} when listing dependencies are unavailable or the caller cancels.
389 * @throws SessionQueryError when the root catalog cannot be read.
390 */
391 listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<SubagentDescendantListEntry[]> {
392 return listSubagentDescendants(this.ctx, rootSessionId, signal)
393 }
394
395 /**
396 * Deliver one browser-authored message to a continuable child through the
397 * exact live direct parent, retaining the caller-minted request identity and
398 * validated browser zone on the accepted message. Success identifies the
399 * message the child's inbox accepted; later execution is independent of this
400 * call. Queue delivery targets a later turn; steer delivery targets the
401 * nearest step and retains the Agent loop's best-effort fallback semantics.
402 * Image parts are admitted and persisted through the attachment store
403 * before delivery, and the child's model must accept image input.
404 * Cold resume at capacity rejects with `subagent/delivery-unavailable`.
405 * @param request - durable address, delivery, minted identity, content, and optional browser zone.
406 * @param signal - carrier cancellation, owning the call until inbox acceptance.
407 * @returns the accepted message's inbox identity.
408 * @throws {RemoteError} `gateway/bad-request`, `subagent/attachment-invalid`,
409 * `subagent/invalid-time-zone`, `subagent/parent-unavailable`,
410 * `subagent/not-resumable`, `subagent/unauthorized`,
411 * `subagent/delivery-unavailable`, `gateway/cancelled`, or `gateway/internal`.
412 */
413 @Remote('prompt')
414 async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise<SubagentPromptReceipt> {
415 const { parentSessionId, childSessionId, clientTimeZone, delivery } = request
416 validateControlRequest('subagent.prompt', request)
417 const canonicalTimeZone = clientTimeZone === undefined
418 ? undefined
419 : canonicalClientTimeZone(clientTimeZone)
420 if (clientTimeZone !== undefined && canonicalTimeZone === undefined) {
421 throw new RemoteError(
422 'subagent/invalid-time-zone',
423 'clientTimeZone must be UTC or a valid IANA Area/Location name',
424 { value: clientTimeZone },
425 )
426 }
427 const parent = this.ctx.get('agents')?.get(parentSessionId)
428 if (parent === undefined) {
429 throw new RemoteError(
430 'subagent/parent-unavailable',
431 `parent session "${parentSessionId}" is not live`,
432 { parentSessionId },
433 )
434 }
435 const source: BrowserPromptSource = {
436 kind: 'user',
437 rpcId: request.requestId,
438 ...(canonicalTimeZone === undefined ? {} : { clientTimeZone: canonicalTimeZone }),
439 }
440 try {
441 // Admission precedes delivery: image parts become durable references
442 // here, so the child inbox only ever accepts Host-persisted attachments.
443 let content: ContentBlock[]
444 if (request.content.every((part): part is { readonly type: 'text'; readonly text: string } => part.type === 'text')) {
445 content = request.content.map(part => ({ type: 'text', text: part.text }))
446 } else {
447 const attachments = this.ctx.get('attachments')
448 if (attachments === undefined) throw new Error('subagent image prompt requires an attachment store')
449 content = await attachments.admitPromptContent(request.content)
450 }
451 return {
452 messageId: await this[deliverSubagentPrompt](
453 parent,
454 childSessionId,
455 content,
456 source,
457 signal,
458 delivery,
459 ),
460 }
461 } catch (error: unknown) {
462 return rejectPrompt(error, childSessionId, signal)
463 }
464 }
465
466 /**
467 * Remote face of {@link interrupt} under one durable parent address. No
468 * catalog, history, persistence, or parent Agent lookup runs: the core
469 * primitive alone authorizes the address against the live Activation, which
470 * is what keeps a live child interruptible while its parent Agent is offline.
471 * Absent, idle, and already-completed targets are accepted no-ops there.
472 * @param childSessionId - durable child session id to interrupt.
473 * @param parentSessionId - durable direct parent whose authority is claimed.
474 * @param mode - required continuable-address discriminator.
475 * @returns acknowledgement that the cancel signal was admitted, not that the target is quiescent.
476 * @throws {RemoteError} `gateway/bad-request` for an empty id,
477 * `subagent/unauthorized` when the address does not own the live target,
478 * otherwise `gateway/internal`.
479 */
480 @Remote('interruptByParent')
481 interruptByParent(
482 childSessionId: SessionId,
483 parentSessionId: SessionId,
484 mode: 'continuable',
485 ): SubagentInterruptReceipt {
486 validateControlRequest('subagent.interrupt', { childSessionId, parentSessionId, mode })
487 try {
488 this.interrupt(childSessionId, { kind: 'user', parentSessionId })
489 } catch (error: unknown) {
490 if (error instanceof SubagentError && error.code === 'UNAUTHORIZED') {
491 throw new RemoteError(
492 'subagent/unauthorized',
493 'subagent does not belong to this parent',
494 { childSessionId },
495 { cause: error },
496 )
497 }
498 throw new RemoteError('gateway/internal', 'subagent interrupt failed', {}, { cause: error })
499 }
500 return { accepted: true }
501 }
502
503 /**
504 * Register a provider under its name. Registration is effect-scoped and HMR
505 * safe; removing a provider blocks new starts but does not revoke runs that
506 * were already returned to their holders.
507 * @param provider - the trusted provider implementation.
508 * @returns the exact Cordis effect disposer.
509 */
510 registerProvider(provider: SubagentProvider): () => void {
511 const name = provider.name
512 // oxlint-disable-next-line typescript/no-misused-promises -- synchronous disposer
513 return this.ctx.effect(function* (this: SubagentRuntime) {
514 if (this.providers.has(name)) {
515 throw new SubagentError(`a subagent provider named "${name}" is already registered`, 'DUPLICATE_PROVIDER')
516 }
517 this.providers.set(name, provider)
518 yield () => {
519 this.providers.delete(name)
520 this.emitLifecycle('subagent/provider-removed', name)
521 }
522 // A throwing added-listener unwinds the yielded rollback, matching the
523 // repository's fail-loud registration semantics.
524 this.ctx.emit('subagent/provider-added', provider)
525 }.bind(this), 'subagents.registerProvider()')
526 }
527
528 /**
529 * Look up a provider by name.
530 * @param name - the provider name.
531 * @returns the provider, or undefined when absent.
532 */
533 getProvider(name: string): SubagentProvider | undefined {
534 return this.providers.get(name)
535 }
536
537 /**
538 * List registered provider names in insertion order.
539 * @returns the registered names.
540 */
541 list(): string[] {
542 return [...this.providers.keys()]
543 }
544
545 /**
546 * Establish a published child on the named provider. Capability and semantic
547 * checks run before delegation. Provider ownership lasts until its promise
548 * fulfills; a rejection therefore has no run for the caller to dispose and
549 * emits no run lifecycle events. Post-publication turn and infrastructure
550 * failures settle through the returned run.
551 * A catalog append failure disposes the run and handles its result rejection;
552 * the caller receives the catalog error even if disposal also fails.
553 * @param name - the provider to use.
554 * @param request - child label, prompt, parent, signal, and optional capabilities.
555 * @returns the published holder-owned run.
556 */
557 async start(name: string, request: SubagentStartRequest): Promise<SubagentRun> {
558 const provider = this.expectProvider(name)
559 this.assertCapabilities(provider, request)
560 assertSubagentMaxDepth(request.maxDepth)
561 if (request.outputSchema !== undefined) assertObjectJsonSchema(request.outputSchema)
562 const descriptor = snapshotSubagentDescriptor({
563 mode: 'one-shot',
564 provider: name,
565 ...request.label !== undefined ? { label: request.label } : {},
566 })
567 const resolved: ResolvedSubagentStartRequest = { ...request, descriptor }
568 const run = await provider.start(resolved)
569 const child = run.localAgent?.session
570 if (child !== undefined) {
571 try {
572 establishCatalogChild(request.parent.session, child.header, descriptor)
573 } catch (error: unknown) {
574 // No caller receives this run; the catalog error owns the failed start.
575 void run.result.catch(() => undefined)
576 try {
577 await run.dispose()
578 } catch (cleanupError: unknown) {
579 this.ctx.logger.warn(
580 `subagent: disposal after catalog append failure also failed: ${String(cleanupError)}`,
581 )
582 }
583 throw error
584 }
585 }
586 return observeRun(this.emitLifecycle, name, request.parent, run)
587 }
588
589 /**
590 * Resolve one provider's detached continuable-creation contribution. Method
591 * presence on the provider IS the capability, so a provider without it is
592 * rejected before the manager reserves any child resources.
593 */
594 private async prepareContinuable(
595 name: string,
596 request: ContinuableCreateRequest,
597 ): Promise<ContinuableCreateSpec> {
598 const provider = this.expectProvider(name)
599 if (provider.prepareContinuable === undefined) {
600 throw new SubagentError(
601 `subagent provider "${provider.name}" does not support continuable children `
602 + '(no prepareContinuable capability)',
603 'UNSUPPORTED_CAPABILITY',
604 )
605 }
606 return provider.prepareContinuable(request)
607 }
608
609 /** Look up a provider for dispatch or fail loud. */
610 private expectProvider(name: string): SubagentProvider {
611 const provider = this.providers.get(name)
612 if (provider === undefined) {
613 throw new SubagentError(`no subagent provider registered for "${name}"`, 'NO_PROVIDER')
614 }
615 return provider
616 }
617
618 /** Resolve the optional continuable-subagent manager or fail loud. */
619 private requireContinuations(): SubagentContinuationManager {
620 if (this.continuations === undefined) {
621 throw new SubagentError(
622 'continuable subagents require the agents service',
623 'CONTINUATION_UNAVAILABLE',
624 )
625 }
626 return this.continuations
627 }
628
629 /**
630 * Build the lifecycle observer for one continuable Activation's residency
631 * epoch, so the manager publishes its edges without owning event dispatch.
632 */
633 private observeActivation(
634 provider: string,
635 childId: SessionId,
636 parent: Agent,
637 ): ActivationObserver {
638 return createActivationObserver(this.emitLifecycle, provider, childId, parent)
639 }
640
641 /** Reject the first requested capability that the provider lacks. */
642 private assertCapabilities(provider: SubagentProvider, request: SubagentStartRequest): void {
643 const needs: { when: boolean; cap: keyof SubagentCapabilities }[] = [
644 { when: request.agentOptions !== undefined, cap: 'agentOptions' },
645 { when: request.outputSchema !== undefined, cap: 'outputSchema' },
646 { when: request.maxDepth !== undefined, cap: 'depthLimit' },
647 { when: request.toolFilter !== undefined, cap: 'toolFilter' },
648 { when: request.persona !== undefined, cap: 'persona' },
649 ]
650 for (const { when, cap } of needs) {
651 if (when && !provider.capabilities[cap]) {
652 throw new SubagentError(
653 `subagent provider "${provider.name}" does not support the "${cap}" capability`,
654 'UNSUPPORTED_CAPABILITY',
655 )
656 }
657 }
658 }
659}
660
661export default SubagentRuntime