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.
This commit is contained in:
Tianyi Cui
2026-07-02 05:52:02 +08:00
parent 8514bddd82
commit 84f3019310
11 changed files with 29 additions and 138 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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