1
/**2
* Service Definition for the subagent capability seam (`ctx.subagents`): a named-provider registry plus a3
* capability-validating asynchronous start API. Providers establish a4
* child before returning its run, so fulfillment is the single publication and5
* ownership-transfer boundary.6
*7
* Multiple providers coexist: each registers under a unique name and callers8
* select one by name.9
*10
* This package owns the Service Definition role of the capability seam. Service Providers11
* (`@deepseek-ai/dsh-subagent-spawn-in-process`, `-fork`, `-acp`) and the model-facing12
* consumer (`@deepseek-ai/dsh-tool-subagent`) are separate packages.13
*14
* Public operations express caller intent: `start` returns one published owned15
* one-shot run, `startContinuable` establishes a durable continuable child, and16
* `sendMessage` steers between adjacent Agents without exposing whether a child17
* is resident. Continuable children never become a {@link SubagentRun}: the18
* continuation manager holds their `AgentHandle` directly and orders every turn19
* through the child's own inbox, so providers contribute only the detached20
* creation spec and see no handle, turn, or teardown. Direct-child21
* discovery reads the parent catalog; descendant discovery recursively reads22
* those child catalogs. Neither read requires the continuation runtime.23
*24
* Same-process providers are trusted typed collaborators. Requests, provider25
* 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-subagent30
*/31
import type { Volatile } from '@deepseek-ai/cordis'33
import { Context } from '@deepseek-ai/cordis'34
import z from '@deepseek-ai/schemastery'35
import type {} from '@deepseek-ai/dsh-attachment'36
import { scopeTarget } from '@deepseek-ai/dsh-scope'37
import type { Scoped } from '@deepseek-ai/dsh-scope'38
import { assertObjectJsonSchema } from '@deepseek-ai/dsh-tools'39
import type { ContentBlock, MessageId, MessageSource } from '@deepseek-ai/dsh-llm'40
import type { Agent } from '@deepseek-ai/dsh-agent'41
import type { SessionId } from '@deepseek-ai/dsh-session'42
import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time'43
import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'44
import {45
rejectPrompt, validateControlRequest,46
} from './control.ts'47
import type {48
SubagentInterruptReceipt,49
SubagentPromptReceipt,50
SubagentPromptRequest,51
SubagentPromptRequestId,52
} from './control-types.ts'53
import 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'68
import { SubagentError } from './error.ts'69
import { assertSubagentMaxDepth } from './depth.ts'70
import { createActivationObserver, createLifecycleEmitter, observeRun } from './lifecycle.ts'71
import type { ActivationObserver, LifecycleEmitter } from './lifecycle.ts'72
import SubagentContinuationManager from './continuation.ts'73
import type { SubagentDelivery } from './inbox.ts'74
import { listChildren as listSubagentChildren, listDescendants as listSubagentDescendants } from './list-children.ts'75
import type { SubagentDescendantListEntry } from './list-children.ts'76
import { installSubagentArchiveAdmission } from './archive-admission.ts'77
import { snapshotSubagentDescriptor } from './descriptor.ts'78
import { subagentIdentityProjectionDefinition, subagentTimingProjectionDefinition } from './projection.ts'79
import { establishCatalogChild, subagentCatalogProjectionDefinition } from './catalog.ts'80
import type { SubagentCatalogEntry } from './projection-types.ts'81
import { deliverSubagentPrompt } from './internal.ts'83
export type {} from './catalog.ts'84
export * from './out-of-process.ts'85
export { AssistantOutputFold, finalAssistantOutput } from './assistant-output.ts'86
export { SubagentRunId } from './types.ts'87
export 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'103
export {104
foldSubagentDescriptor,105
snapshotSubagentDescriptor,106
SUBAGENT_DESCRIPTOR_VERSION,107
} from './descriptor.ts'108
export type {109
ContinuableSubagentDescriptorData,110
ContinuableSubagentDescriptorInput,111
OneShotSubagentDescriptorData,112
OneShotSubagentDescriptorInput,113
SubagentDescriptorData,114
SubagentDescriptorInput,115
} from './descriptor.ts'116
export type { SubagentCatalogEntry } from './projection-types.ts'117
export { SubagentError } from './error.ts'118
export { settleRun } from './run-settlement.ts'119
export { assertSubagentMaxDepth, delegationDepthOf } from './depth.ts'120
export {121
appendDelegatedPolicyOverrides,122
applyChildComposition,123
captureDelegatedPolicyOverrides,124
childSessionMeta,125
parentAgentOptionsForDelegation,126
resolveChildAgentOptions,127
resolveChildDepth,128
SubagentDepthError,129
} from './child-agent.ts'130
export type { ChildComposition, DelegatedPolicyOverrides } from './child-agent.ts'131
export type { AgentMessageSource, SubagentSettledMessageSource } from './continuation-messages.ts'132
export type * from './control-types.ts'133
export type { SubagentDescendantListEntry } from './list-children.ts'134
export type { SubagentRunEndInfo, SubagentRunInfo } from './types.ts'135
export type { SubagentIdentityProjection, SubagentTimingProjection } from './projection-types.ts'137
declare module '@deepseek-ai/cordis' {138
interface Context {139
subagents: SubagentRuntime140
}142
interface Events {143
/**144
* A provider became resolvable in the registry.145
* @param provider - the registered provider.146
* @mode emit147
*/148
'subagent/provider-added'(provider: SubagentProvider): void149
/**150
* A provider left the registry. Accepted runs remain holder-owned.151
* @param name - the provider name that no longer resolves.152
* @mode emit153
*/154
'subagent/provider-removed'(name: string): void155
/**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 a159
* parent-scoped listener observes only its own delegations. Paired with160
* `subagent/end`.161
* @param info - the provider and published child identity.162
* @mode emit163
*/164
'subagent/start'(this: Scoped<SubagentRuntime>, info: SubagentRunInfo): void165
/**166
* A published child settled. Scope-filtered dispatch uses the same delegating167
* parent carrier as `subagent/start`, so the lifecycle pair reaches the168
* same scoped audience.169
* @param info - the run identity and terminal outcome.170
* @mode emit171
*/172
'subagent/end'(this: Scoped<SubagentRuntime>, info: SubagentRunEndInfo): void173
}174
}176
/**177
* Durable attribution of one browser-authored follow-up. The Session178
* Controller declares this `user-rpc` message source and depends on this179
* package, so the fields are spelled here: `MessageSource`'s `user` member180
* accepts the record and the correlation id rides the durable message the181
* Client reconciles its optimistic prompt against.182
*/183
interface BrowserPromptSource {184
readonly kind: 'user'185
readonly rpcId: SubagentPromptRequestId186
readonly clientTimeZone?: string187
}189
/** Host configuration for continuable subagent capacity. */190
export 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
}197
/** Named provider registry with one-shot runs, durable discovery, and continuable-child operations. */198
export 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 | undefined205
/**206
* The contained lifecycle-edge publisher. Built here because scoped dispatch207
* keys its carrier by this exact service instance, whose own context filter208
* composes into the carrier.209
*/210
private readonly emitLifecycle: LifecycleEmitter212
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 = manager221
childCtx.effect(() => () => {222
/* v8 ignore else -- one injected binding owns the slot until its fiber disposes. */223
if (this.continuations === manager) this.continuations = undefined224
}, 'subagents.continuationBinding()')225
})226
ctx.inject(['sessionProjections'], (projectionCtx) => {227
const projections = projectionCtx.sessionProjections228
projections.register(subagentCatalogProjectionDefinition)229
projections.register(subagentTimingProjectionDefinition)230
projections.register(subagentIdentityProjectionDefinition)231
})232
// Archive admission: this runtime is the owner that knows which live233
// children descend from a Session and how a parent stops them.234
ctx.inject(['agents'], (agentsCtx: Context) => { installSubagentArchiveAdmission(agentsCtx) })235
}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 undefined244
if (configured !== undefined) return configured245
const depth = this.config.maxDepth.get()246
assertSubagentMaxDepth(depth)247
return depth248
}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 the253
* turn to start or for the message to reach the Session log; any earlier254
* 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
}263
/**264
* Steer one model-authored message to the sender's direct parent or direct265
* 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 from267
* persistence. The service derives durable sender attribution from the exact268
* 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
}286
/**287
* Deliver one host-protocol message to a direct continuable child.288
* Symbol-keyed so host adapters can preserve their own source descriptors without289
* 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
}311
/**312
* Interrupt one live continuable child's current turn under a human parent313
* address or an exact live ancestor Agent. Fire-and-return: the cancel314
* signal is issued before this returns, but the target may keep running315
* 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 FIFO318
* 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 a320
* 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 the324
* live target.325
*/326
interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void {327
this.continuations?.interrupt(targetSessionId, authority)328
}330
/**331
* Close continuable admission below exact live parent Agents, stop only their332
* visible descendant Activations synchronously, then await admitted scoped333
* materializations and release those forests child-first. The scoped cutoff334
* lasts until each exact parent leaves the registry; unrelated parent trees335
* 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.continuations342
// Absent continuation services means nothing was ever materialized.343
if (manager === undefined) return344
await manager.drainDescendants(parents)345
}347
/**348
* Release selected resident continuable direct children of one exact live349
* 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 a355
* different parent or the supplied parent identity is stale.356
*/357
async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void> {358
const manager = this.continuations359
if (manager === undefined) return360
await manager.drainChildren(parent, childIds)361
}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
}376
/**377
* Recursively list reachable parent catalogs in stable pre-order, preserving378
* each catalog's event order. Each row carries its catalog parent and depth;379
* one-shot and unknown-mode children remain traversal nodes. Unknown modes380
* produce unsupported diagnostics. Unreadable child catalogs produce corrupt381
* 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 Agent384
* 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
}395
/**396
* Deliver one browser-authored message to a continuable child through the397
* exact live direct parent, retaining the caller-minted request identity and398
* validated browser zone on the accepted message. Success identifies the399
* message the child's inbox accepted; later execution is independent of this400
* call. Queue delivery targets a later turn; steer delivery targets the401
* nearest step and retains the Agent loop's best-effort fallback semantics.402
* Image parts are admitted and persisted through the attachment store403
* 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 } = request416
validateControlRequest('subagent.prompt', request)417
const canonicalTimeZone = clientTimeZone === undefined418
? undefined419
: 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 references442
// 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
}466
/**467
* Remote face of {@link interrupt} under one durable parent address. No468
* catalog, history, persistence, or parent Agent lookup runs: the core469
* primitive alone authorizes the address against the live Activation, which470
* 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
}503
/**504
* Register a provider under its name. Registration is effect-scoped and HMR505
* safe; removing a provider blocks new starts but does not revoke runs that506
* 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.name512
// oxlint-disable-next-line typescript/no-misused-promises -- synchronous disposer513
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 the523
// repository's fail-loud registration semantics.524
this.ctx.emit('subagent/provider-added', provider)525
}.bind(this), 'subagents.registerProvider()')526
}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
}537
/**538
* List registered provider names in insertion order.539
* @returns the registered names.540
*/541
list(): string[] {542
return [...this.providers.keys()]543
}545
/**546
* Establish a published child on the named provider. Capability and semantic547
* checks run before delegation. Provider ownership lasts until its promise548
* fulfills; a rejection therefore has no run for the caller to dispose and549
* emits no run lifecycle events. Post-publication turn and infrastructure550
* 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?.session570
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 error584
}585
}586
return observeRun(this.emitLifecycle, name, request.parent, run)587
}589
/**590
* Resolve one provider's detached continuable-creation contribution. Method591
* presence on the provider IS the capability, so a provider without it is592
* 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
}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 provider616
}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.continuations627
}629
/**630
* Build the lifecycle observer for one continuable Activation's residency631
* 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
}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
}661
export default SubagentRuntime