/** * 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 */ import type { Context } from 'cordis' import { defineTool } from '@deepseek-ai/dsh-tools' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-subagent' export const name = 'tool-subagent-control' export const inject = ['tools', 'subagents'] /** * Register the `send_message` and `interrupt_agent` tools. * @param ctx - context carrying the tool registry and subagent service. */ export function apply(ctx: Context): void { ctx.tools.register(defineTool({ name: 'send_message', description: 'Send a message to a background subagent by its subagent id, continuing the same conversation. It ' + 'becomes the subagent\'s next turn: if it is still working, the message waits until its current turn ' + 'finishes, so it cannot redirect work already underway. This call returns no answer from the ' + 'subagent — only confirmation that the message was delivered — so use it to give it more work. A ' + 'failure means the message was NOT delivered.', parameters: { subagent_id: { type: 'string', required: true, description: 'The subagent id returned when the background subagent was started.', }, message: { type: 'string', required: true, description: 'The message to deliver to the subagent.', }, }, output: { schema: { type: 'object', additionalProperties: false, properties: { messageId: { type: 'string', required: true }, }, }, render: (args, _value) => [{ type: 'text', text: `message queued as the next turn for subagent ${args.subagent_id}`, }], }, async execute(args, exec) { const parent = exec.agent if (!parent) { // Parent authority requires an exact live calling agent. throw new Error('send_message requires a calling agent (exec.agent was undefined)') } const message: ContentBlock[] = [{ type: 'text', text: args.message }] const messageId = await ctx.subagents.followup( parent, SessionId(args.subagent_id), message, { source: { kind: 'coordinator', form: 'relay', senderSessionId: parent.id }, signal: exec.signal, }, ) 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 }) }, })) }