Преглед на файлове

Fix review findings: lifecycle containment, configurable tool name, coverage, type catalog

Address four findings from the first Codex review round:

- Contain subagent/start|end listener throws (emitContainedStart/End): a
  thrown lifecycle listener could escape SubagentService.start() before the
  caller received the live run to dispose it (a leaked child), and a thrown
  subagent/end listener could surface as an unhandled rejection on the detached
  result-settle hook. Both emits now log-and-contain, mirroring the agent
  registry's agent/created|disposed containment.
- Make the model-facing tool name configurable (Config.toolName, default
  subagent). The docs say to load dsh-tool-subagent once per provider to expose
  multiple transports, but the hardcoded name made the second load throw a
  duplicate-tool-name error; a distinct toolName per load is now required and
  documented.
- Reach the per-file 100% coverage gate: tests for the subagent/end error
  branch, lifecycle-listener containment, every stopReasonError arm + the
  merge-extensible default, the multi-provider toolName path, agentOptions
  forwarding, and the direct-apply schema-bypass fallbacks.
- Document the seam vocabulary in docs/core-data-structures/subagent.md with
  verbatim type-equiv blocks + manifest entries, and link it from core.md (a
  brand-new core/seam type the doc-sync gate cannot detect on its own).
Tianyi Cui преди 3 месеца
родител
ревизия
25eccdaedc

+ 1 - 0
docs/core-data-structures/core.md

@@ -20,6 +20,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
 | [persistence.md](persistence.md) | the durability seam: `SessionPersistence`, JSONL + SQLite backends, `session/flush`, crash recovery, `SessionHeader` |
 | [tools.md](tools.md) | `ToolDefinition` full fields, the schema DSL, `ToolExecution`/`ToolResult`, tool-presentation UI types, the `tools/execute` waterfall |
 | [bash.md](bash.md) | the bash executor seam: `BashExecRequest`/`Spec`, `BashRunResult`, background `BashTask`s |
+| [subagent.md](subagent.md) | the subagent seam: the named-provider registry, `SubagentStartRequest`/`Result`/`Run`, the start-time-vs-runtime capability split |
 
 > Type definitions on this page are pasted **verbatim** from source and drift-checked by `pnpm run verify-type-equiv` (see [development.md](../development.md#documenting-types-verbatim-ts-type-equiv)). Inline JSDoc is omitted for readability; follow the source link for the full contracts.
 

+ 88 - 0
docs/core-data-structures/subagent.md

@@ -0,0 +1,88 @@
+# Subagent
+
+The subagent seam — an agent delegating work to a child agent. Like [bash](bash.md) it is **one optional capability**, not part of the agent-loop spine, so its vocabulary lives here rather than in [core.md](core.md). But it differs from every other seam on one axis: **multiple provider implementations coexist** in one context, registered by name (`ctx.subagents`), where bash allows only one executor. The registry shape mirrors the [LLM adapter registry](llm-streaming.md), not the single-service bash executor.
+
+Interface: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Implementations are sibling packages (`dsh-subagent-spawn`, `-fork`, `-acp`); the model-facing consumer is [dsh-tool-subagent](../../packages/subagent/tool-subagent). The proposal and rationale: [the subagent RFC](../rfc/proposed/feature/2026-06-21-subagent-capability-seam.md).
+
+Source: [`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts)
+
+## Two kinds of capability, discovered two ways
+
+A provider advertises its **start-time** features on a static descriptor the service checks BEFORE a run exists; a request that needs one the provider lacks is rejected loud (`SubagentError('UNSUPPORTED_CAPABILITY')`), never accepted-then-ignored. **Runtime** features (steering, resume) are instead optional methods on [`SubagentRun`](#a-live-run-subagentrun) — the method's presence IS the capability, and TS narrowing is the discovery mechanism.
+
+```ts type-equiv
+interface SubagentCapabilities {
+  outputSchema: boolean
+  depthLimit: boolean
+  toolFilter: boolean
+}
+```
+
+## The start request
+
+What a caller asks for when starting a subagent. The tool layer builds this from the model's `{ description, prompt }` plus its own config; the service validates the start-time capabilities against the named provider, then passes it to `provider.start`. `parent` is REQUIRED — in-process backends read `parent.session.header` for the working directory, the `parentSession` lineage, and the delegation depth. The three optional fields (`outputSchema`, `maxDepth`, `toolFilter`) each gate on the matching `SubagentCapabilities` flag.
+
+```ts type-equiv
+interface SubagentStartRequest {
+  prompt: ContentBlock[]
+  parent: Agent
+  signal?: AbortSignal
+  agentOptions?: AgentOptions
+  outputSchema?: SchemaSpec
+  maxDepth?: number
+  toolFilter?: { allow?: string[]; deny?: string[] }
+}
+```
+
+## The terminal result: `SubagentResult`
+
+The outcome of a run, resolved by `SubagentRun.result`. `structured` is present iff the request carried an `outputSchema` AND the provider honored it. A non-`completed` `stopReason` means `output` may be partial — the consumer maps it to an `isError` tool result rather than reporting partial output as success.
+
+```ts type-equiv
+interface SubagentResult {
+  output: ContentBlock[]
+  structured?: unknown
+  stopReason: SubagentStopReason
+}
+```
+
+`SubagentStopReason` is a [merge-extensible derived union](core.md#the-map--derived-union-pattern) — a backend may add variants, so consumers branch on the known cases and treat an unknown terminal reason as a failure:
+
+```ts type-equiv
+interface SubagentStopReasonMap {
+  completed: 'completed'
+  aborted: 'aborted'
+  error: 'error'
+  'max-tokens': 'max-tokens'
+  refusal: 'refusal'
+}
+```
+
+## A live run: `SubagentRun`
+
+The handle the consumer holds while a child executes. The consumer awaits `result`, may `cancel` mid-flight, and MUST `dispose` on every path to reach child quiescence (no leaked idle child / session). `result` does NOT reject on a child-level failure — a model/transport failure resolves with `stopReason: 'error'` — so the consumer maps a non-`completed` reason to an `isError` result; it rejects only on an infrastructure fault the seam cannot represent. `sendMessage` and `resume` are OPTIONAL: a provider that supports the runtime capability defines the method; one that doesn't omits it.
+
+```ts type-equiv
+interface SubagentRun {
+  readonly id: AgentId
+  readonly result: Promise<SubagentResult>
+  cancel(reason?: string): void
+  dispose(): Promise<void>
+  sendMessage?(content: ContentBlock[]): void
+  resume?(content: ContentBlock[]): SubagentRun
+}
+```
+
+## The provider seam: `SubagentProvider`
+
+One transport for running a child agent. Implementations register under a unique name via `SubagentService.registerProvider`; multiple coexist in one context. The service validates every requested start-time capability before calling `start`, so an implementation may assume e.g. `request.maxDepth` is honorable when present.
+
+```ts type-equiv
+interface SubagentProvider {
+  readonly name: string
+  readonly capabilities: SubagentCapabilities
+  start(request: SubagentStartRequest): SubagentRun
+}
+```
+
+The service (`ctx.subagents`) emits `subagent/start` when a run begins and `subagent/end` when it settles (see the [events catalog](../cordis-catalog/events-and-services.md)). Both emits contain a thrown listener (logged, never propagated) so one bad subscriber can neither strand a live run nor surface as an unhandled rejection on the detached settle hook.

+ 1 - 1
docs/rfc/proposed/feature/2026-06-21-subagent-capability-seam.md

@@ -56,7 +56,7 @@ The `dsh-tool-subagent` consumer awaits `run.result` and returns the child's fin
 
 ### Provider selection is config, not model-facing
 
-`dsh-tool-subagent` binds to exactly one provider name (`Config.provider`); the model sees only `{ description, prompt }`. To expose more than one transport, load the tool plugin more than once, each bound to a different provider. The *service* holds the multi-provider registry; the *tool* picks one — no provider/type parameter in the schema this cut.
+`dsh-tool-subagent` binds to exactly one provider name (`Config.provider`); the model sees only `{ description, prompt }`. To expose more than one transport, load the tool plugin more than once, each bound to a different provider and a distinct `toolName` (the tool registry rejects a duplicate name). The *service* holds the multi-provider registry; the *tool* picks one — no provider/type parameter in the schema this cut.
 
 ## Plan (three PRs, each converged with Codex separately)
 

+ 36 - 4
packages/subagent/subagent/src/index.ts

@@ -153,19 +153,51 @@ export class SubagentService extends Service {
     this.assertCapabilities(provider, request)
 
     const run = provider.start(request)
-    this.ctx.emit('subagent/start', { provider: name, id: run.id })
+    // CONTAIN lifecycle-listener throws: the run is already live, so a throwing
+    // `subagent/start` listener must NOT escape `start()` (the caller would
+    // never receive the run to dispose it — a leaked child). Emit defensively
+    // and log a thrown listener, mirroring the agent registry's `agent/created`
+    // /`agent/disposed` containment.
+    this.emitContainedStart({ provider: name, id: run.id })
     // Emit `subagent/end` when the run settles. The result promise does not
     // reject on a child-level failure (it resolves with stopReason 'error'),
     // so a rejection here is an infrastructure fault — surface its stop reason
     // as 'error' for the telemetry event without swallowing the rejection
-    // (the consumer still observes it via `run.result`).
+    // (the consumer still observes it via `run.result`). Containment also keeps
+    // a thrown `subagent/end` listener from becoming an unhandled rejection on
+    // this detached `.then`.
     void run.result.then(
-      (result) => { this.ctx.emit('subagent/end', { provider: name, id: run.id, stopReason: result.stopReason }) },
-      () => { this.ctx.emit('subagent/end', { provider: name, id: run.id, stopReason: 'error' }) },
+      (result) => { this.emitContainedEnd({ provider: name, id: run.id, stopReason: result.stopReason }) },
+      () => { this.emitContainedEnd({ provider: name, id: run.id, stopReason: 'error' }) },
     )
     return run
   }
 
+  /**
+   * Emit `subagent/start`, containing a thrown listener (log, never propagate)
+   * so one bad subscriber cannot strand the already-live run before the caller
+   * receives it to dispose.
+   */
+  private emitContainedStart(info: SubagentRunInfo): void {
+    try {
+      this.ctx.emit('subagent/start', info)
+    } catch (error: unknown) {
+      this.ctx.logger.warn(`subagent: subagent/start listener threw: ${String(error)}`)
+    }
+  }
+
+  /**
+   * Emit `subagent/end`, containing a thrown listener so it cannot surface as an
+   * unhandled rejection on the detached result-settle hook.
+   */
+  private emitContainedEnd(info: SubagentRunEndInfo): void {
+    try {
+      this.ctx.emit('subagent/end', info)
+    } catch (error: unknown) {
+      this.ctx.logger.warn(`subagent: subagent/end listener threw: ${String(error)}`)
+    }
+  }
+
   /**
    * Reject a request that needs a start-time capability the provider lacks.
    * Each optional request field maps to one {@link SubagentCapabilities} flag;

+ 54 - 0
packages/subagent/subagent/tests/service.spec.ts

@@ -173,6 +173,60 @@ describe('SubagentService', () => {
     expect(ended).toHaveBeenCalledWith(expect.objectContaining({ provider: 'events', id: run.id, stopReason: 'completed' }))
   })
 
+  it('emits subagent/end with stopReason "error" when the run result promise rejects', async () => {
+    const ctx = new Context()
+    await ctx.plugin(SubagentService)
+    // A provider whose run.result REJECTS (an infrastructure fault — the seam
+    // contract says child-level failures resolve with stopReason 'error', but a
+    // rejection is still surfaced as an 'error' telemetry event).
+    ctx.subagents.registerProvider({
+      name: 'rejecter',
+      capabilities: NO_CAPS,
+      start: () => ({
+        id: AgentId('rej-child'),
+        result: Promise.reject(new Error('infra fault')),
+        cancel() {},
+        dispose: async () => {},
+      }),
+    })
+
+    const ended = vi.fn()
+    ctx.on('subagent/end', ended)
+    const run = ctx.subagents.start('rejecter', baseRequest())
+    // Observe (and swallow) the rejection the consumer would see, then let the
+    // detached `.then` settle the telemetry emit.
+    await run.result.catch(() => {})
+    await Promise.resolve()
+    expect(ended).toHaveBeenCalledWith(expect.objectContaining({ provider: 'rejecter', id: run.id, stopReason: 'error' }))
+  })
+
+  it('contains a throwing subagent/start listener so start() still returns the run', async () => {
+    const ctx = new Context()
+    await ctx.plugin(SubagentService)
+    ctx.subagents.registerProvider(new StubProvider('contain'))
+    // A bad subscriber must not strand the live run: start() returns it anyway.
+    ctx.on('subagent/start', () => { throw new Error('bad start listener') })
+
+    const run = ctx.subagents.start('contain', baseRequest())
+    expect(run.id).toBeDefined()
+    await expect(run.result).resolves.toMatchObject({ stopReason: 'completed' })
+  })
+
+  it('contains a throwing subagent/end listener (no unhandled rejection on the settle hook)', async () => {
+    const ctx = new Context()
+    await ctx.plugin(SubagentService)
+    ctx.subagents.registerProvider(new StubProvider('contain-end'))
+    ctx.on('subagent/end', () => { throw new Error('bad end listener') })
+
+    const run = ctx.subagents.start('contain-end', baseRequest())
+    await run.result
+    // Let the detached `.then` + the contained emit run; a thrown listener here
+    // must be swallowed (logged), not escape as an unhandled rejection.
+    await Promise.resolve()
+    await Promise.resolve()
+    expect(run.id).toBeDefined()
+  })
+
   it('SubagentError extends the shared HarnessError base', () => {
     const err = new SubagentError('boom', 'NO_PROVIDER')
     expect(err).toBeInstanceOf(HarnessError)

+ 2 - 1
packages/subagent/tool-subagent/README.md

@@ -4,11 +4,12 @@ The model-facing `subagent` tool: delegate a self-contained task to a child agen
 
 ## Provider selection is config, not model-facing
 
-This plugin binds to **exactly one** provider (`Config.provider`). The model sees only `{ description, prompt }` — there is no provider/type parameter in the schema. To expose more than one transport, load the plugin more than once, each bound to a different provider. Keeping selection in config (not the schema) is the deliberate split: the *service* holds a multi-provider registry; the *tool* picks one.
+This plugin binds to **exactly one** provider (`Config.provider`). The model sees only `{ description, prompt }` — there is no provider/type parameter in the schema. To expose more than one transport, load the plugin more than once, each bound to a different provider **and a distinct `toolName`** (the tool registry rejects a duplicate name, so a second load that kept the default `subagent` name would throw). Keeping selection in config (not the schema) is the deliberate split: the *service* holds a multi-provider registry; the *tool* picks one.
 
 | Config key | Meaning |
 |---|---|
 | `provider` (required) | The `ctx.subagents` provider name to start runs on (`spawn`, `fork`, `acp`, …). |
+| `toolName` | The model-facing tool name to register (default `subagent`). Set a distinct value per load when exposing multiple providers, e.g. `subagent` + `subagent_acp`. |
 | `agentOptions` | Default per-child `{ model?, systemPrompt? }` applied to every spawned child. |
 
 ## Lifecycle (synchronous collect)

+ 10 - 1
packages/subagent/tool-subagent/src/index.ts

@@ -35,6 +35,14 @@ export const inject = ['tools', 'subagents']
 export interface Config {
   /** The `ctx.subagents` provider name to start runs on (e.g. `spawn`, `acp`). */
   provider: string
+  /**
+   * The model-facing tool name to register (default `subagent`). To expose more
+   * than one transport, load this plugin once per provider — each load MUST set
+   * a distinct `toolName` (the tool registry rejects a duplicate name), e.g.
+   * `{ provider: 'spawn', toolName: 'subagent' }` and
+   * `{ provider: 'acp', toolName: 'subagent_acp' }`.
+   */
+  toolName?: string
   /**
    * Default per-child agent options (model, system prompt) applied to every
    * spawned child. Omitted fields fall back to the child loop's own defaults.
@@ -44,6 +52,7 @@ export interface Config {
 
 export const Config: z<Config> = z.object({
   provider: z.string().required(),
+  toolName: z.string().default('subagent'),
   agentOptions: z.object({
     model: z.string(),
     systemPrompt: z.string(),
@@ -85,7 +94,7 @@ function stopReasonError(result: SubagentResult): string | undefined {
 
 export function apply(ctx: Context, config: Config): void {
   ctx.tools.register(defineTool({
-    name: 'subagent',
+    name: config.toolName ?? 'subagent',
     description:
       'Delegate a self-contained task to a subagent (a separate agent that works in its own context) '
       + 'and return its final result. Use this to offload focused, independent work — research, a scoped '

+ 113 - 3
packages/subagent/tool-subagent/tests/tool-subagent.spec.ts

@@ -68,11 +68,121 @@ describe('dsh-tool-subagent', () => {
     expect(Object.keys(props).sort()).toEqual(['description', 'prompt'])
   })
 
-  it('maps a non-completed stop reason to an isError result (not partial success)', async () => {
-    const ctx = await setup({ provider: 'mock' }, { stopReason: 'refusal' })
+  it.each([
+    { stopReason: 'aborted' as const, fragment: 'cancelled' },
+    { stopReason: 'error' as const, fragment: 'failed' },
+    { stopReason: 'max-tokens' as const, fragment: 'token limit' },
+    { stopReason: 'refusal' as const, fragment: 'declined' },
+  ])('maps stop reason $stopReason to an isError result (not partial success)', async ({ stopReason, fragment }) => {
+    const ctx = await setup({ provider: 'mock' }, { stopReason })
     const result = await callSubagent(ctx, { description: 'd', prompt: 'p' })
     expect(result.isError).toBe(true)
-    expect(text(result)).toContain('declined')
+    expect(text(result)).toContain(fragment)
+  })
+
+  it('registers under a configurable toolName so multiple providers can coexist', async () => {
+    // The defining multi-provider use case: two loads, two distinct tool names,
+    // each bound to a different provider — the tool registry rejects duplicate
+    // names, so a configurable name is what makes this work.
+    const ctx = new Context()
+    await ctx.plugin(SystemPrompt)
+    await ctx.plugin(ToolRegistry)
+    await ctx.plugin(SubagentService)
+    await ctx.plugin(mock, { name: 'spawn', reply: 'from spawn' })
+    await ctx.plugin(mock, { name: 'acp', reply: 'from acp' })
+    await ctx.plugin(tool, { provider: 'spawn', toolName: 'subagent' })
+    await ctx.plugin(tool, { provider: 'acp', toolName: 'subagent_acp' })
+
+    const names = ctx.tools.schemas().map(s => s.name).filter(n => n.startsWith('subagent')).sort()
+    expect(names).toEqual(['subagent', 'subagent_acp'])
+
+    const viaSpawn = await ctx.tools.execute({ callId: CallId('c-spawn'), name: 'subagent', arguments: { description: 'd', prompt: 'p' }, agent: fakeAgent() })
+    const viaAcp = await ctx.tools.execute({ callId: CallId('c-acp'), name: 'subagent_acp', arguments: { description: 'd', prompt: 'p' }, agent: fakeAgent() })
+    expect(text(viaSpawn)).toBe('from spawn')
+    expect(text(viaAcp)).toBe('from acp')
+  })
+
+  it('treats an unknown (plugin-added) stop reason as an isError result', async () => {
+    // SubagentStopReason is merge-extensible; the tool's stopReasonError default
+    // arm must treat an unrecognized terminal reason as a failure, not success.
+    const ctx = new Context()
+    await ctx.plugin(SystemPrompt)
+    await ctx.plugin(ToolRegistry)
+    await ctx.plugin(SubagentService)
+    ctx.subagents.registerProvider({
+      name: 'weird',
+      capabilities: { outputSchema: false, depthLimit: false, toolFilter: false },
+      start: () => ({
+        id: AgentId('weird-child'),
+        result: Promise.resolve({ output: [{ type: 'text', text: 'partial' }], stopReason: 'frobnicated' as never }),
+        cancel() {},
+        dispose: async () => {},
+      }),
+    })
+    await ctx.plugin(tool, { provider: 'weird' })
+
+    const result = await callSubagent(ctx, { description: 'd', prompt: 'p' })
+    expect(result.isError).toBe(true)
+    expect(text(result)).toContain('abnormally')
+  })
+
+  it('forwards configured agentOptions into the start request', async () => {
+    // Cover the `config.agentOptions ? … : {}` spread: a provider that captures
+    // the request lets us assert the agentOptions reached it.
+    let seen: { agentOptions?: { model?: string } } | undefined
+    const ctx = new Context()
+    await ctx.plugin(SystemPrompt)
+    await ctx.plugin(ToolRegistry)
+    await ctx.plugin(SubagentService)
+    ctx.subagents.registerProvider({
+      name: 'capture',
+      capabilities: { outputSchema: false, depthLimit: false, toolFilter: false },
+      start: (request) => {
+        seen = request
+        return {
+          id: AgentId('capture-child'),
+          result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }),
+          cancel() {},
+          dispose: async () => {},
+        }
+      },
+    })
+    await ctx.plugin(tool, { provider: 'capture', agentOptions: { model: 'child-model', systemPrompt: 'be terse' } })
+
+    await callSubagent(ctx, { description: 'd', prompt: 'p' })
+    expect(seen?.agentOptions).toEqual({ model: 'child-model', systemPrompt: 'be terse' })
+  })
+
+  it('defaults toolName and omits agentOptions when apply() is called directly (schema bypass)', async () => {
+    // `ctx.plugin` validates+defaults config first (toolName→'subagent', the
+    // agentOptions object→{}), so the runtime `?? 'subagent'` fallback and the
+    // no-agentOptions branch are only reachable via a direct apply() that
+    // bypasses schemastery — the same pattern acp-agent uses for its defaults.
+    let seen: { agentOptions?: unknown } | undefined
+    const ctx = new Context()
+    await ctx.plugin(SystemPrompt)
+    await ctx.plugin(ToolRegistry)
+    await ctx.plugin(SubagentService)
+    ctx.subagents.registerProvider({
+      name: 'bare',
+      capabilities: { outputSchema: false, depthLimit: false, toolFilter: false },
+      start: (request) => {
+        seen = request
+        return {
+          id: AgentId('bare-child'),
+          result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }),
+          cancel() {},
+          dispose: async () => {},
+        }
+      },
+    })
+    // Direct apply with only `provider` — no toolName, no agentOptions.
+    tool.apply(ctx, { provider: 'bare' })
+    await new Promise(r => setTimeout(r, 10))
+
+    expect(ctx.tools.schemas().some(s => s.name === 'subagent')).toBe(true)
+    await callSubagent(ctx, { description: 'd', prompt: 'p' })
+    expect(seen?.agentOptions).toBeUndefined()
   })
 
   it('fails loud when invoked without a calling agent', async () => {

+ 6 - 0
packages/support/subagent-mock/tests/subagent-mock.spec.ts

@@ -45,6 +45,12 @@ describe('dsh-subagent-mock', () => {
     await expect(run.result).resolves.toMatchObject({ structured: { answer: 42 } })
   })
 
+  it('defaults structured output to { reply } when outputSchema is requested but no structured value is configured', async () => {
+    const ctx = await mount({ reply: 'fallback reply' })
+    const run = ctx.subagents.start('mock', baseRequest({ outputSchema: { answer: { type: 'number' } } }))
+    await expect(run.result).resolves.toMatchObject({ structured: { reply: 'fallback reply' } })
+  })
+
   it('omits structured output when outputSchema capability is off', async () => {
     const ctx = await mount({ capabilities: { outputSchema: false } })
     // The service rejects an outputSchema request against a no-cap provider, so

+ 8 - 1
scripts/type-equiv.manifest.json

@@ -35,6 +35,13 @@
     { "doc": "docs/core-data-structures/bash.md", "symbol": "BashRunResult", "source": "packages/bash/bash/src/types.ts" },
     { "doc": "docs/core-data-structures/bash.md", "symbol": "CollectedOutput", "source": "packages/bash/bash/src/types.ts" },
     { "doc": "docs/core-data-structures/bash.md", "symbol": "BashTask", "source": "packages/bash/bash/src/types.ts" },
-    { "doc": "docs/core-data-structures/bash.md", "symbol": "BashTaskRead", "source": "packages/bash/bash/src/types.ts" }
+    { "doc": "docs/core-data-structures/bash.md", "symbol": "BashTaskRead", "source": "packages/bash/bash/src/types.ts" },
+
+    { "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentCapabilities", "source": "packages/subagent/subagent/src/types.ts" },
+    { "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentStartRequest", "source": "packages/subagent/subagent/src/types.ts" },
+    { "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentResult", "source": "packages/subagent/subagent/src/types.ts" },
+    { "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentStopReasonMap", "source": "packages/subagent/subagent/src/types.ts" },
+    { "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentRun", "source": "packages/subagent/subagent/src/types.ts" },
+    { "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentProvider", "source": "packages/subagent/subagent/src/types.ts" }
   ]
 }