feat(subagent): enrich subagent/start + subagent/end lifecycle events (observe-only)
A hooks bridge translating SubagentStart/SubagentStop needs to know WHICH kind of subagent ran and WHAT it produced — Claude Code's hooks carry subagent_type and the child's final message. Enrich the existing lifecycle emits to match, observe-only: - agentType: an optional caller-supplied subagent-kind label (CC's subagent_type), added to SubagentStartRequest and carried VERBATIM onto both subagent/start (SubagentRunInfo) and subagent/end (SubagentRunEndInfo). The seam never interprets it. dsh-tool-subagent threads it from a new optional Config.agentType, so a deployment exposing multiple subagent kinds (one tool load per kind) labels each. - lastAssistantMessage: the child's final output (SubagentResult.output), added to SubagentRunEndInfo on the settle path so an observer sees what the subagent produced without holding the run. Absent on the reject path (no result produced). Strictly observe-only: both events stay plain emits (subagent/end fires from a detached .then and awaits no listener). A control-flow subagent/end (awaited waterfall returning a decision) would need the emit→waterfall reshape, awaiting listeners before settling, and a provider resume capability — deferred to the background/steering redesign (FIXME(subagent-continuation) anchors it). RFC: implemented/feature/2026-06-30-subagent-observe-enrich.md.
This commit is contained in:
@@ -20,11 +20,21 @@
|
||||
* 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`.
|
||||
* 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
|
||||
* before settling, and a `resume` capability on the in-process provider — part
|
||||
* of the deferred background/steering redesign, NOT this observe-only cut.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-subagent
|
||||
*/
|
||||
|
||||
import { Context, Service } from 'cordis'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import type { AgentId } from '@deepseek-ai/dsh-agent'
|
||||
import type {
|
||||
SubagentCapabilities,
|
||||
@@ -72,6 +82,13 @@ 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). */
|
||||
@@ -80,8 +97,18 @@ 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']
|
||||
/**
|
||||
* The child's final assistant output ({@link SubagentResult.output}), carried
|
||||
* onto the end event so an observer sees WHAT the subagent produced without
|
||||
* holding the run. Absent when the run rejected at the infrastructure level
|
||||
* (no {@link SubagentResult} was produced — the seam only knows `stopReason:
|
||||
* 'error'`).
|
||||
*/
|
||||
lastAssistantMessage?: ContentBlock[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,17 +187,22 @@ 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.
|
||||
this.emitLifecycle('subagent/start', { provider: name, id: run.id })
|
||||
// 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 })
|
||||
// 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`). Per-listener
|
||||
// containment also keeps a thrown `subagent/end` listener from becoming an
|
||||
// unhandled rejection on this detached `.then`.
|
||||
// (the consumer still observes it via `run.result`). On the resolve path the
|
||||
// child's final output rides on the event (lastAssistantMessage); on the
|
||||
// reject path there is no SubagentResult, so only the stop reason is known.
|
||||
// Per-listener containment also keeps a thrown `subagent/end` listener from
|
||||
// becoming an unhandled rejection on this detached `.then`.
|
||||
void run.result.then(
|
||||
(result) => { this.emitLifecycle('subagent/end', { provider: name, id: run.id, stopReason: result.stopReason }) },
|
||||
() => { this.emitLifecycle('subagent/end', { provider: name, id: run.id, stopReason: 'error' }) },
|
||||
(result) => { this.emitLifecycle('subagent/end', { provider: name, id: run.id, ...agentType, stopReason: result.stopReason, lastAssistantMessage: result.output }) },
|
||||
() => { this.emitLifecycle('subagent/end', { provider: name, id: run.id, ...agentType, stopReason: 'error' }) },
|
||||
)
|
||||
return run
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user