feat(subagent): let ancestors interrupt descendants
interrupt_agent(agent_id) passes the calling agent as the ancestor authority for ctx.subagents.interrupt(); the service verifies live registry identity and recorded lineage, so a direct child or deeper descendant stops with the same generic parameter while send_message keeps its exact-direct-parent authority. Discovery: list_agents gains an optional scope. descendants walks the new SubagentService.listDescendants() — one lineage trace flattened in stable pre-order across ordinary and one-shot intermediates, each entry carrying its verified parentId and depth — and every status now comes from the live Agent registry (running/idle/complete). Refs #1535
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
/**
|
||||
* The globally named `send_message` tool: a thin model-facing adapter over
|
||||
* `ctx.subagents.followup()`. It performs no lifecycle routing of its own —
|
||||
* residency and cold resume belong to the subagent service — and it lives apart
|
||||
* from the provider-bound `@deepseek-ai/dsh-tool-subagent` instances so multiple
|
||||
* delegation tools share one control tool.
|
||||
* The globally named `send_message` and `interrupt_agent` tools: thin
|
||||
* model-facing adapters over `ctx.subagents.followup()` and
|
||||
* `ctx.subagents.interrupt()`. They perform no lifecycle routing of their own —
|
||||
* residency, cold resume, and interrupt authorization belong to the subagent
|
||||
* service — and they live apart from the provider-bound
|
||||
* `@deepseek-ai/dsh-tool-subagent` instances so multiple delegation tools share
|
||||
* one control surface.
|
||||
* @module @deepseek-ai/dsh-tool-subagent-control
|
||||
*/
|
||||
|
||||
@@ -17,7 +19,7 @@ export const name = 'tool-subagent-control'
|
||||
export const inject = ['tools', 'subagents']
|
||||
|
||||
/**
|
||||
* Register the `send_message` tool.
|
||||
* Register the `send_message` and `interrupt_agent` tools.
|
||||
* @param ctx - context carrying the tool registry and subagent service.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
@@ -73,4 +75,46 @@ export function apply(ctx: Context): void {
|
||||
return { messageId }
|
||||
},
|
||||
}))
|
||||
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'interrupt_agent',
|
||||
description:
|
||||
'Request cancellation of a background agent\'s current turn by its agent id. The target may be your '
|
||||
+ 'direct child or a deeper agent created under you. Only the current turn stops: messages already '
|
||||
+ 'queued for the agent stay parked until a later send_message, agents it started keep running, and '
|
||||
+ 'the agent itself stays available for follow-ups. This call returns as soon as the stop request is '
|
||||
+ 'accepted, so the target may keep running briefly; interrupting an agent that already finished is '
|
||||
+ 'an accepted no-op.',
|
||||
parameters: {
|
||||
agent_id: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The agent id of the running agent to interrupt.',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
accepted: { type: 'boolean', required: true },
|
||||
},
|
||||
},
|
||||
render: (args, _value) => [{
|
||||
type: 'text',
|
||||
text: `interrupt requested for agent ${args.agent_id}`,
|
||||
}],
|
||||
},
|
||||
execute(args, exec) {
|
||||
const caller = exec.agent
|
||||
if (!caller) {
|
||||
// Ancestor authority requires an exact live calling agent.
|
||||
throw new Error('interrupt_agent requires a calling agent (exec.agent was undefined)')
|
||||
}
|
||||
// The service authorizes the exact live caller against the target's
|
||||
// recorded lineage; the tool adds no authority of its own.
|
||||
ctx.subagents.interrupt(SessionId(args.agent_id), { kind: 'ancestor', agent: caller })
|
||||
return Promise.resolve({ accepted: true })
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user