Bläddra i källkod

refactor(subagent): drop the agentType lifecycle field

Address review: `agentType` was a Claude-Code concept (`subagent_type`) that
does not fit our own subagent seam — nothing in the harness interprets it, and
its only consumer was the CC-dialect hook bridge. Rather than let a foreign
concept sit on the core seam, remove it:

- `SubagentStartRequest`, `SubagentRunInfo`, `SubagentRunEndInfo`: drop the
  `agentType` field; the `subagent/start`/`subagent/end` payloads now carry
  `provider`/`id` (+ end `stopReason`/`lastAssistantMessage`) only.
- `dsh-tool-subagent`: drop `Config.agentType` and its request plumbing.
- Tests: keep the lastAssistantMessage / clone-containment / reject-path
  coverage (rewritten to not assert agentType); delete the two tool-subagent
  tests that only exercised agentType forwarding (dead behavior).
- Docs: retitle + rewrite the subagent-observe-enrich RFC to the one shipped
  enrichment (lastAssistantMessage), with a note on why agentType was dropped;
  update rfc/README index title, both subagent READMEs, and the
  core-data-structures/subagent.md type-equiv block + prose; regenerate catalog.

The CC bridge (PR-F) will feed Claude Code's own default matcher value
"general-purpose" for its SubagentStart/Stop agent_type matcher instead.
Tianyi Cui 3 månader sedan
förälder
incheckning
84f3019310

+ 1 - 1
docs/cordis-catalog/events-and-services.md

@@ -445,7 +445,7 @@ list(): string[]
 start(name: string, request: SubagentStartRequest): SubagentRun
 ```
 
-Source: [`packages/subagent/subagent/src/index.ts:130`](../../packages/subagent/subagent/src/index.ts)
+Source: [`packages/subagent/subagent/src/index.ts:121`](../../packages/subagent/subagent/src/index.ts)
 
 ### `ctx.systemPrompt` — `SystemPrompt`
 

+ 1 - 2
docs/core-data-structures/subagent.md

@@ -25,7 +25,6 @@ What a caller asks for when starting a subagent. The tool layer builds this from
 ```ts type-equiv
 interface SubagentStartRequest {
   prompt: ContentBlock[]
-  agentType?: string
   parent: Agent
   signal?: AbortSignal
   agentOptions?: AgentOptions
@@ -86,7 +85,7 @@ interface SubagentProvider {
 }
 ```
 
-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 payloads carry the caller's optional `agentType` label (verbatim from the request — Claude Code's `subagent_type`); `subagent/end` additionally carries `lastAssistantMessage` (the child's final `output`) on the settle path, so an observer sees WHAT the subagent produced without holding the run (absent when the run rejected at the infrastructure level — no result was produced). These are **observe-only** enrichments: both events are plain `emit`s (the `subagent/end` fires from a detached `.then` after the result settles and awaits no listener), so a subscriber observes but cannot change the run. Both emits contain a thrown listener **per listener** (logged, never propagated): one bad subscriber can neither strand a live run, surface as an unhandled rejection on the detached settle hook, nor starve the listeners registered after it.
+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)). `subagent/end` carries `lastAssistantMessage` (the child's final `output`) on the settle path, so an observer sees WHAT the subagent produced without holding the run (absent when the run rejected at the infrastructure level — no result was produced). These are **observe-only** events: both are plain `emit`s (the `subagent/end` fires from a detached `.then` after the result settles and awaits no listener), so a subscriber observes but cannot change the run. Both emits contain a thrown listener **per listener** (logged, never propagated): one bad subscriber can neither strand a live run, surface as an unhandled rejection on the detached settle hook, nor starve the listeners registered after it.
 
 ## In-process backends: depth and seed
 

+ 1 - 1
docs/rfc/README.md

@@ -87,7 +87,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
 | [ACP subagent backend (out-of-process delegation)](implemented/feature/2026-06-22-acp-subagent-backend.md) | 2026-06-22 |
 | [The `todo_write` tool — model task list as event-sourced session state](implemented/feature/2026-06-29-todo-write-tool.md) | 2026-06-29 |
 | [Interception seams — the typed-Decision surface a hook programs against](implemented/feature/2026-06-30-interception-seams.md) | 2026-06-30 |
-| [Subagent lifecycle enrichment — agentType + lastAssistantMessage (observe-only)](implemented/feature/2026-06-30-subagent-observe-enrich.md) | 2026-06-30 |
+| [Subagent lifecycle enrichment — lastAssistantMessage (observe-only)](implemented/feature/2026-06-30-subagent-observe-enrich.md) | 2026-06-30 |
 
 ### Simplification
 

+ 12 - 9
docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.md

@@ -1,22 +1,25 @@
-# RFC: Subagent lifecycle enrichment — agentType + lastAssistantMessage (observe-only)
+# RFC: Subagent lifecycle enrichment — lastAssistantMessage (observe-only)
 
 Status: implemented (accepted 2026-06-30)
 
 <!-- XXX: legacy ADR/RFC body format, not yet normalized to a unified RFC template. -->
+<!-- An earlier draft also added an `agentType` subagent-kind label (the harness
+     analogue of CC's `subagent_type`) to the request + both lifecycle payloads.
+     It was dropped in review: it is a Claude-Code concept that does not fit our
+     own seam (nothing here interprets it, and the only consumer was a CC-dialect
+     bridge). The CC bridge instead feeds Claude Code's own default matcher value
+     `"general-purpose"` for its SubagentStart/Stop `agent_type` matcher. So this
+     RFC ships ONE enrichment: `lastAssistantMessage`. -->
 
 ## Context
 
-The hooks subsystem ([interception seams RFC](2026-06-30-interception-seams.md)) lets a plugin observe and gate the agent at lifecycle points. Claude Code and Codex both expose **SubagentStart / SubagentStop** hooks, and CC's carry a `subagent_type` (which named subagent kind ran) and the subagent's final message. The harness already emits `subagent/start` and `subagent/end` lifecycle events ([the subagent capability-seam](2026-06-21-subagent-capability-seam.md)), but their payloads were minimal (`provider`, `id`, and on end `stopReason`) — not enough for a hooks bridge to report which KIND of subagent ran, or WHAT it produced, without separately reaching for the live run.
+The hooks subsystem ([interception seams RFC](2026-06-30-interception-seams.md)) lets a plugin observe and gate the agent at lifecycle points. Claude Code and Codex both expose **SubagentStart / SubagentStop** hooks, and CC's carry the subagent's final message. The harness already emits `subagent/start` and `subagent/end` lifecycle events ([the subagent capability-seam](2026-06-21-subagent-capability-seam.md)), but their payloads were minimal (`provider`, `id`, and on end `stopReason`) — not enough for a hooks bridge to report WHAT a subagent produced without separately reaching for the live run.
 
-This RFC enriches those two payloads. It is deliberately **observe-only**: no control-flow change, no waterfall, no `start()` restructure. A run-affecting subagent-stop decision (continuation, injection that changes the run) is a separate, larger redesign and stays out of scope.
+This RFC enriches the end payload. It is deliberately **observe-only**: no control-flow change, no waterfall, no `start()` restructure. A run-affecting subagent-stop decision (continuation, injection that changes the run) is a separate, larger redesign and stays out of scope.
 
 ## Decision
 
-Add two pieces of information to the subagent lifecycle surface:
-
-1. **`agentType` — a caller-supplied subagent-kind label**, the harness analogue of CC's `subagent_type`. It is optional on `SubagentStartRequest`, carried VERBATIM onto both `subagent/start` (`SubagentRunInfo`) and `subagent/end` (`SubagentRunEndInfo`). The seam never interprets it. The model-facing `dsh-tool-subagent` tool threads it from a new optional `Config.agentType`, so a deployment that exposes multiple subagent kinds (one tool load per kind) labels each. Absent when the caller does not distinguish kinds (the spread omits the key — `exactOptionalPropertyTypes`-correct).
-
-2. **`lastAssistantMessage` — the child's final output**, added to `SubagentRunEndInfo`. On the settle path it is a DEEP CLONE of `SubagentResult.output` (so an observer sees WHAT the subagent produced without holding the run). On the REJECT path (an infrastructure fault where no `SubagentResult` was produced — the seam only knows `stopReason: 'error'`) it is absent. The clone is load-bearing for observe-only: the `subagent/end` emit fires from a detached `.then` registered *before* `start()` returns, i.e. before the caller's own `await run.result` continuation — handing listeners the same array reference would let a mutating listener corrupt the caller's `SubagentResult.output`. `structuredClone` makes the event a read-only view (a regression test mutates the event's array and asserts the caller's result is untouched).
+**Add `lastAssistantMessage` — the child's final output — to `SubagentRunEndInfo`.** On the settle path it is a DEEP CLONE of `SubagentResult.output` (so an observer sees WHAT the subagent produced without holding the run). On the REJECT path (an infrastructure fault where no `SubagentResult` was produced — the seam only knows `stopReason: 'error'`) it is absent. The clone is load-bearing for observe-only: the `subagent/end` emit fires from a detached `.then` registered *before* `start()` returns, i.e. before the caller's own `await run.result` continuation — handing listeners the same array reference would let a mutating listener corrupt the caller's `SubagentResult.output`. `structuredClone` makes the event a read-only view (a regression test mutates the event's array and asserts the caller's result is untouched); a clone failure is contained (logged, the event still fires without `lastAssistantMessage`) rather than becoming an unhandled rejection on the detached `.then`.
 
 Both events stay plain **`emit`s**. `subagent/end` fires from a detached `.then` on `run.result` and awaits no listener, so it is genuinely observe-only by construction — a `subagent/start` listener can still reach the live child via `ctx.agents.get(info.id)` and `inject()` into it; a `subagent/end` listener can only observe (the run has settled). Per-listener containment (already in place) keeps one bad subscriber from stranding a live run or surfacing as an unhandled rejection on the detached settle hook.
 
@@ -26,4 +29,4 @@ A control-flow `subagent/end` (an awaited waterfall returning a stop/continue de
 
 ## Consequences
 
-A hooks bridge (or a native plugin) can now translate SubagentStart/SubagentStop faithfully: it reports `agentType`, matches its hook config on it, and forwards the child's `lastAssistantMessage` to a SubagentStop handler — all by subscribing to the existing emits, no new control-flow surface. The vocabulary addition is documented in [docs/core-data-structures/subagent.md](../../../core-data-structures/subagent.md) (the `SubagentStartRequest` type-equiv block + the events prose) and the two subagent READMEs; the catalog is regenerated. No production behavior changes — the events fire exactly as before, with two more (optional) fields on their payloads — so no snapshot or e2e change is needed.
+A hooks bridge (or a native plugin) can now forward the child's `lastAssistantMessage` to a SubagentStop handler by subscribing to the existing emits — no new control-flow surface. The vocabulary addition is documented in [docs/core-data-structures/subagent.md](../../../core-data-structures/subagent.md) (the events prose) and the two subagent READMEs; the catalog is regenerated. No production behavior changes — the events fire exactly as before, with one more (optional) field on the end payload — so no snapshot or e2e change is needed.

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

@@ -32,7 +32,7 @@ Unlike the bash seam (one executor per context, second load throws), **multiple
 
 `provider.start(request)` returns a `SubagentRun`: a handle with a `result` promise, `cancel()`, `dispose()`, and the optional runtime methods. `result` resolves with a `SubagentResult` (`output`, optional `structured`, `stopReason`) — it 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` tool result. The consumer MUST `dispose()` on every path (success, error, abort) to reach child quiescence and avoid leaking an idle child / session.
 
-The service emits `subagent/start` (payload `SubagentRunInfo`) and `subagent/end` (payload `SubagentRunEndInfo`) around the run — both **observe-only** (plain `emit`s; `subagent/end` fires from a detached `.then` and awaits no listener). Both payloads carry the request's optional `agentType` label (Claude Code's `subagent_type`, verbatim — the seam never interprets it); `subagent/end` additionally carries `lastAssistantMessage` (a deep clone of the child's final `output`) on the settle path, absent when the run rejected at the infrastructure level. The clone keeps the surface observe-only: the end emit fires from a detached `.then` before the caller's `await run.result` resumes, so a shared reference would let a mutating listener corrupt the caller's result. A `subagent/start` listener can still reach the live child via `ctx.agents.get(info.id)`; a `subagent/end` listener can only observe (the run has settled). Any run-affecting decision (continuation, injection that changes the run) is out of scope for this observe-only surface.
+The service emits `subagent/start` (payload `SubagentRunInfo`) and `subagent/end` (payload `SubagentRunEndInfo`) around the run — both **observe-only** (plain `emit`s; `subagent/end` fires from a detached `.then` and awaits no listener). `subagent/end` carries `lastAssistantMessage` (a deep clone of the child's final `output`) on the settle path, absent when the run rejected at the infrastructure level. The clone keeps the surface observe-only: the end emit fires from a detached `.then` before the caller's `await run.result` resumes, so a shared reference would let a mutating listener corrupt the caller's result. A `subagent/start` listener can still reach the live child via `ctx.agents.get(info.id)`; a `subagent/end` listener can only observe (the run has settled). Any run-affecting decision (continuation, injection that changes the run) is out of scope for this observe-only surface.
 
 ## Scope (first cut)
 

+ 6 - 18
packages/subagent/subagent/src/index.ts

@@ -20,9 +20,9 @@
  * semantics are deferred to a future redesign that unifies long-running-tool
  * handling across subagents and bash.
  *
- * The `subagent/start` / `subagent/end` lifecycle events carry an enriched but
- * OBSERVE-ONLY payload (`agentType`, and on end `lastAssistantMessage`) — see
- * `docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.md`.
+ * The `subagent/start` / `subagent/end` lifecycle events carry an OBSERVE-ONLY
+ * payload; `subagent/end` additionally carries the child's `lastAssistantMessage`
+ * — see `docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.md`.
  * FIXME(subagent-continuation): a control-flow `subagent/end` (an awaited
  * waterfall returning a stop/continue decision, like the other interception
  * seams) would require reshaping this emit into a waterfall, awaiting listeners
@@ -82,13 +82,6 @@ export interface SubagentRunInfo {
   provider: string
   /** The child agent's id. */
   id: AgentId
-  /**
-   * The caller's subagent-kind label, carried verbatim from
-   * {@link SubagentStartRequest.agentType} (Claude Code's `subagent_type`).
-   * Absent when the caller did not supply one. An observer (a hooks bridge,
-   * a UI) reports or matches on it; the seam never interprets it.
-   */
-  agentType?: string
 }
 
 /** Outcome detail for a settled subagent run (the `subagent/end` payload). */
@@ -97,8 +90,6 @@ export interface SubagentRunEndInfo {
   provider: string
   /** The child agent's id. */
   id: AgentId
-  /** The caller's subagent-kind label (see {@link SubagentRunInfo.agentType}). */
-  agentType?: string
   /** The terminal stop reason. */
   stopReason: SubagentResult['stopReason']
   /**
@@ -187,10 +178,7 @@ export class SubagentService extends Service {
     // acceptable. `ctx.emit` halts the dispatch on the first throw, so a single
     // surrounding try/catch is not enough — each listener is invoked and
     // contained individually.
-    // Carry the caller's subagent-kind label verbatim onto both lifecycle events
-    // (absent when not supplied — the spread omits the key for exactOptionalPropertyTypes).
-    const agentType = request.agentType !== undefined ? { agentType: request.agentType } : {}
-    this.emitLifecycle('subagent/start', { provider: name, id: run.id, ...agentType })
+    this.emitLifecycle('subagent/start', { 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
@@ -220,9 +208,9 @@ export class SubagentService extends Service {
         } catch (error: unknown) {
           this.ctx.logger.warn(`subagent: could not clone ${name} output for subagent/end: ${String(error)}`)
         }
-        this.emitLifecycle('subagent/end', { provider: name, id: run.id, ...agentType, stopReason: result.stopReason, ...lastAssistantMessage !== undefined ? { lastAssistantMessage } : {} })
+        this.emitLifecycle('subagent/end', { provider: name, id: run.id, stopReason: result.stopReason, ...lastAssistantMessage !== undefined ? { lastAssistantMessage } : {} })
       },
-      () => { this.emitLifecycle('subagent/end', { provider: name, id: run.id, ...agentType, stopReason: 'error' }) },
+      () => { this.emitLifecycle('subagent/end', { provider: name, id: run.id, stopReason: 'error' }) },
     )
     return run
   }

+ 0 - 9
packages/subagent/subagent/src/types.ts

@@ -40,15 +40,6 @@ export interface SubagentCapabilities {
 export interface SubagentStartRequest {
   /** The task/prompt for the child agent (a user message in the child session). */
   prompt: ContentBlock[]
-  /**
-   * Optional caller-supplied LABEL for the kind of subagent (e.g. `code-reviewer`,
-   * `researcher`) — the harness analogue of Claude Code's `subagent_type`. The
-   * seam does not interpret it; it is carried verbatim onto the `subagent/start`
-   * and `subagent/end` lifecycle events so an observer (a hooks bridge, a UI) can
-   * report or match on which kind of subagent ran. Absent when the caller does
-   * not distinguish subagent kinds.
-   */
-  agentType?: string
   /**
    * The spawning ("parent") agent — the one whose tool call started this
    * subagent. REQUIRED: in-process backends read `parent.session.header` for

+ 7 - 32
packages/subagent/subagent/tests/service.spec.ts

@@ -173,7 +173,7 @@ describe('SubagentService', () => {
     expect(ended).toHaveBeenCalledWith(expect.objectContaining({ provider: 'events', id: run.id, stopReason: 'completed' }))
   })
 
-  it('carries agentType (from the request) onto both lifecycle events, and lastAssistantMessage onto end', async () => {
+  it('carries lastAssistantMessage (the child output) onto the end event', async () => {
     const ctx = new Context()
     await ctx.plugin(SubagentService)
     ctx.subagents.registerProvider(new StubProvider(
@@ -187,42 +187,19 @@ describe('SubagentService', () => {
     ctx.on('subagent/start', started)
     ctx.on('subagent/end', ended)
 
-    const run = ctx.subagents.start('enriched', baseRequest({ agentType: 'code-reviewer' }))
-    expect(started).toHaveBeenCalledWith(expect.objectContaining({ provider: 'enriched', id: run.id, agentType: 'code-reviewer' }))
+    const run = ctx.subagents.start('enriched', baseRequest())
+    expect(started).toHaveBeenCalledWith(expect.objectContaining({ provider: 'enriched', id: run.id }))
 
     await run.result
     await Promise.resolve()
     expect(ended).toHaveBeenCalledWith(expect.objectContaining({
       provider: 'enriched',
       id: run.id,
-      agentType: 'code-reviewer',
       stopReason: 'completed',
       lastAssistantMessage: [{ type: 'text', text: 'the child answer' }],
     }))
   })
 
-  it('omits agentType when the request supplied none', async () => {
-    const ctx = new Context()
-    await ctx.plugin(SubagentService)
-    ctx.subagents.registerProvider(new StubProvider('plain'))
-
-    const started = vi.fn()
-    const ended = vi.fn()
-    ctx.on('subagent/start', started)
-    ctx.on('subagent/end', ended)
-
-    const run = ctx.subagents.start('plain', baseRequest())
-    await run.result
-    await Promise.resolve()
-
-    const startInfo = started.mock.calls[0]![0] as Record<string, unknown>
-    const endInfo = ended.mock.calls[0]![0] as Record<string, unknown>
-    expect('agentType' in startInfo).toBe(false)
-    expect('agentType' in endInfo).toBe(false)
-    // lastAssistantMessage IS present on a resolved end (the child's output).
-    expect(endInfo.lastAssistantMessage).toEqual([{ type: 'text', text: 'ok' }])
-  })
-
   it('observe-only: a subagent/end listener mutating lastAssistantMessage cannot corrupt the caller\'s result', async () => {
     // The subagent/end emit fires from a detached `.then` registered before
     // start() returns — i.e. BEFORE the caller's own `await run.result`
@@ -267,14 +244,13 @@ describe('SubagentService', () => {
 
     const ended = vi.fn()
     ctx.on('subagent/end', ended)
-    const run = ctx.subagents.start('rej', baseRequest({ agentType: 'researcher' }))
+    const run = ctx.subagents.start('rej', baseRequest())
     await run.result.catch(() => {})
     await Promise.resolve()
 
     const endInfo = ended.mock.calls[0]![0] as Record<string, unknown>
     expect(endInfo.stopReason).toBe('error')
-    expect(endInfo.agentType).toBe('researcher') // agentType still carried on reject
-    expect('lastAssistantMessage' in endInfo).toBe(false) // but no output exists
+    expect('lastAssistantMessage' in endInfo).toBe(false) // no output exists on reject
   })
 
   it('contains a structuredClone failure: emits subagent/end without lastAssistantMessage (no unhandled rejection)', async () => {
@@ -282,7 +258,7 @@ describe('SubagentService', () => {
     // containment. An uncloneable output (here a content block carrying a
     // function) would otherwise throw and become an unhandled rejection on the
     // detached `.then`. The handler must instead log and emit the event WITHOUT
-    // lastAssistantMessage, still carrying the real stopReason/agentType.
+    // lastAssistantMessage, still carrying the real stopReason.
     const ctx = new Context()
     await ctx.plugin(SubagentService)
     const warn = vi.fn(); ctx.logger.warn = warn as never
@@ -301,13 +277,12 @@ describe('SubagentService', () => {
 
     const ended = vi.fn()
     ctx.on('subagent/end', ended)
-    const run = ctx.subagents.start('unclone', baseRequest({ agentType: 'researcher' }))
+    const run = ctx.subagents.start('unclone', baseRequest())
     await run.result
     await Promise.resolve()
 
     const endInfo = ended.mock.calls[0]![0] as Record<string, unknown>
     expect(endInfo.stopReason).toBe('completed')        // the real outcome is preserved
-    expect(endInfo.agentType).toBe('researcher')
     expect('lastAssistantMessage' in endInfo).toBe(false) // clone failed → omitted, not crashed
     expect(warn).toHaveBeenCalledWith(expect.stringContaining('could not clone'))
   })

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

@@ -11,7 +11,6 @@ This plugin binds to **exactly one** provider (`Config.provider`). The model see
 | `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. |
-| `agentType` | Optional subagent-kind label (Claude Code's `subagent_type`) stamped on every run's `subagent/start`/`subagent/end` events, so an observer (a hooks bridge, a UI) can report or match on which kind ran. Set a distinct value per load when exposing multiple subagent kinds. |
 
 ## Lifecycle (synchronous collect)
 

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

@@ -48,14 +48,6 @@ export interface Config {
    * spawned child. Omitted fields fall back to the child loop's own defaults.
    */
   agentOptions?: AgentOptions
-  /**
-   * Optional subagent-kind LABEL stamped on every run this tool starts (Claude
-   * Code's `subagent_type`). Carried onto the `subagent/start`/`subagent/end`
-   * lifecycle events so an observer can report or match on which kind of
-   * subagent ran. A deployment that exposes multiple subagent kinds (one tool
-   * load per kind) sets a distinct `agentType` per load; omit when undifferentiated.
-   */
-  agentType?: string
 }
 
 export const Config: z<Config> = z.object({
@@ -65,7 +57,6 @@ export const Config: z<Config> = z.object({
     model: z.string(),
     systemPrompt: z.string(),
   }),
-  agentType: z.string(),
 })
 
 /**
@@ -137,7 +128,6 @@ export function apply(ctx: Context, config: Config): void {
         parent,
         ...exec.signal ? { signal: exec.signal } : {},
         ...config.agentOptions ? { agentOptions: config.agentOptions } : {},
-        ...config.agentType !== undefined ? { agentType: config.agentType } : {},
       }
 
       const run: SubagentRun = ctx.subagents.start(config.provider, request)

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

@@ -153,60 +153,6 @@ describe('dsh-tool-subagent', () => {
     expect(seen?.agentOptions).toEqual({ model: 'child-model', systemPrompt: 'be terse' })
   })
 
-  it('forwards a configured agentType into the start request (observed on the lifecycle events)', async () => {
-    let seen: { agentType?: string } | undefined
-    const starts: { agentType?: string }[] = []
-    const ctx = new Context()
-    await ctx.plugin(SystemPrompt)
-    await ctx.plugin(ToolRegistry)
-    await ctx.plugin(SubagentService)
-    ctx.on('subagent/start', info => void starts.push(info))
-    ctx.subagents.registerProvider({
-      name: 'typed',
-      capabilities: { outputSchema: false, depthLimit: false, toolFilter: false },
-      start: (request) => {
-        seen = request
-        return {
-          id: AgentId('typed-child'),
-          result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }),
-          cancel() {},
-          dispose: async () => {},
-        }
-      },
-    })
-    await ctx.plugin(tool, { provider: 'typed', agentType: 'code-reviewer' })
-
-    await callSubagent(ctx, { description: 'd', prompt: 'p' })
-    // The config agentType reaches the request, and the service stamps it on the event.
-    expect(seen?.agentType).toBe('code-reviewer')
-    expect(starts[0]?.agentType).toBe('code-reviewer')
-  })
-
-  it('omits agentType from the request when none is configured', async () => {
-    let seen: { agentType?: string } | undefined
-    const ctx = new Context()
-    await ctx.plugin(SystemPrompt)
-    await ctx.plugin(ToolRegistry)
-    await ctx.plugin(SubagentService)
-    ctx.subagents.registerProvider({
-      name: 'untyped',
-      capabilities: { outputSchema: false, depthLimit: false, toolFilter: false },
-      start: (request) => {
-        seen = request
-        return {
-          id: AgentId('untyped-child'),
-          result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }),
-          cancel() {},
-          dispose: async () => {},
-        }
-      },
-    })
-    await ctx.plugin(tool, { provider: 'untyped' })
-
-    await callSubagent(ctx, { description: 'd', prompt: 'p' })
-    expect(seen !== undefined && 'agentType' in seen).toBe(false)
-  })
-
   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