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

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