@morlay/dsh-subagent 0.0.6 → 0.0.8

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