From 84f3019310c6b8734bdfa36095802528ee29cbbe Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 2 Jul 2026 05:52:02 +0800 Subject: [PATCH] refactor(subagent): drop the agentType lifecycle field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/cordis-catalog/events-and-services.md | 2 +- docs/core-data-structures/subagent.md | 3 +- docs/rfc/README.md | 2 +- .../2026-06-30-subagent-observe-enrich.md | 21 ++++---- packages/subagent/subagent/README.md | 2 +- packages/subagent/subagent/src/index.ts | 24 +++------ packages/subagent/subagent/src/types.ts | 9 ---- .../subagent/subagent/tests/service.spec.ts | 39 +++----------- packages/subagent/tool-subagent/README.md | 1 - packages/subagent/tool-subagent/src/index.ts | 10 ---- .../tool-subagent/tests/tool-subagent.spec.ts | 54 ------------------- 11 files changed, 29 insertions(+), 138 deletions(-) diff --git a/docs/cordis-catalog/events-and-services.md b/docs/cordis-catalog/events-and-services.md index e6dd0881fa..ae549af20f 100644 --- a/docs/cordis-catalog/events-and-services.md +++ b/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` diff --git a/docs/core-data-structures/subagent.md b/docs/core-data-structures/subagent.md index ca85830808..1d998b60b7 100644 --- a/docs/core-data-structures/subagent.md +++ b/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 diff --git a/docs/rfc/README.md b/docs/rfc/README.md index 29267f2ef2..4e77e322c2 100644 --- a/docs/rfc/README.md +++ b/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 diff --git a/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.md b/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.md index a1893ed99c..b67ede3ab6 100644 --- a/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.md +++ b/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) + ## 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. diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index 6db26272c7..3b72971dc1 100644 --- a/packages/subagent/subagent/README.md +++ b/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) diff --git a/packages/subagent/subagent/src/index.ts b/packages/subagent/subagent/src/index.ts index ee6540f44a..926c22d0c8 100644 --- a/packages/subagent/subagent/src/index.ts +++ b/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 } diff --git a/packages/subagent/subagent/src/types.ts b/packages/subagent/subagent/src/types.ts index 55ef04a8a9..fb60d5667c 100644 --- a/packages/subagent/subagent/src/types.ts +++ b/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 diff --git a/packages/subagent/subagent/tests/service.spec.ts b/packages/subagent/subagent/tests/service.spec.ts index 9db02ebe34..3a8807ad0d 100644 --- a/packages/subagent/subagent/tests/service.spec.ts +++ b/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 - const endInfo = ended.mock.calls[0]![0] as Record - 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 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 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')) }) diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 996e44d96e..1bb48f29ff 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/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) diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index cb92035e88..05490127ea 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/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 = z.object({ @@ -65,7 +57,6 @@ export const Config: z = 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) diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index 0b088b65db..2f40cd6f8c 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/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