index.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. /**
  2. * The globally named `send_message` and `interrupt_agent` tools: thin
  3. * model-facing adapters over `ctx.subagents.sendMessage()` and
  4. * `ctx.subagents.interrupt()`. They perform no lifecycle routing of their own —
  5. * residency, cold resume, and interrupt authorization belong to the subagent
  6. * service — and they live apart from the provider-bound
  7. * `@deepseek-ai/dsh-tool-subagent` instances so multiple delegation tools share
  8. * one control API.
  9. * @module @deepseek-ai/dsh-tool-subagent-control
  10. */
  11. import type { Context } from '@deepseek-ai/cordis'
  12. import { brandString } from '@deepseek-ai/dsh-brand'
  13. import { defineTool } from '@deepseek-ai/dsh-tools'
  14. import type { ContentBlock } from '@deepseek-ai/dsh-llm'
  15. import type { SessionId } from '@deepseek-ai/dsh-session'
  16. import type {} from '@deepseek-ai/dsh-subagent'
  17. import { markAdjacentAgentSendMessageTool } from '@deepseek-ai/dsh-subagent/internal'
  18. export const name = 'tool-subagent-control'
  19. export const inject = ['tools', 'subagents']
  20. /**
  21. * Register the `send_message` and `interrupt_agent` tools.
  22. * @param ctx - context carrying the tool registry and subagent service.
  23. */
  24. export function apply(ctx: Context): void {
  25. ctx.tools.register(markAdjacentAgentSendMessageTool(defineTool({
  26. name: 'send_message',
  27. description:
  28. 'Send a message to a direct continuable child by its agent id. If you are a resident continuable child, '
  29. + 'you may also target your direct parent. If the target is still working, the message steers its nearest step; '
  30. + 'if it is idle, the message starts a turn. This call returns no answer from the agent — only confirmation '
  31. + 'that the message was delivered. A failure means the message was NOT delivered.',
  32. parameters: {
  33. agent_id: {
  34. type: 'string',
  35. required: true,
  36. description: 'The agent id of your direct continuable child, or your direct parent when you are a resident continuable child.',
  37. },
  38. message: {
  39. type: 'string',
  40. required: true,
  41. description: 'The message to deliver to the agent.',
  42. },
  43. },
  44. output: {
  45. schema: {
  46. type: 'object',
  47. additionalProperties: false,
  48. properties: {
  49. messageId: { type: 'string', required: true },
  50. },
  51. },
  52. render: (args, _value) => [{
  53. type: 'text',
  54. text: `message delivered to agent ${args.agent_id}`,
  55. }],
  56. },
  57. async execute(args, exec) {
  58. const sender = exec.agent
  59. if (!sender) {
  60. throw new Error('send_message requires a calling agent (exec.agent was undefined)')
  61. }
  62. const message: ContentBlock[] = [{ type: 'text', text: args.message }]
  63. const messageId = await ctx.subagents.sendMessage(
  64. sender,
  65. brandString<SessionId>(args.agent_id),
  66. message,
  67. { signal: exec.signal },
  68. )
  69. return { messageId }
  70. },
  71. })))
  72. ctx.tools.register(defineTool({
  73. name: 'interrupt_agent',
  74. description:
  75. 'Request cancellation of a background agent\'s current turn by its agent id. The target may be your '
  76. + 'direct child or a deeper agent created under you. Only the current turn stops: messages already '
  77. + 'queued for the agent stay parked until a later send_message, agents it started keep running, and '
  78. + 'the agent itself stays available for follow-ups. This call returns as soon as the stop request is '
  79. + 'accepted, so the target may keep running briefly; interrupting an agent that already finished is '
  80. + 'an accepted no-op.',
  81. parameters: {
  82. agent_id: {
  83. type: 'string',
  84. required: true,
  85. description: 'The agent id of the running agent to interrupt.',
  86. },
  87. },
  88. output: {
  89. schema: {
  90. type: 'object',
  91. additionalProperties: false,
  92. properties: {
  93. accepted: { type: 'boolean', required: true },
  94. },
  95. },
  96. render: (args, _value) => [{
  97. type: 'text',
  98. text: `interrupt requested for agent ${args.agent_id}`,
  99. }],
  100. },
  101. execute(args, exec) {
  102. const caller = exec.agent
  103. if (!caller) {
  104. // Ancestor authority requires an exact live calling agent.
  105. throw new Error('interrupt_agent requires a calling agent (exec.agent was undefined)')
  106. }
  107. // The service authorizes the exact live caller against the target's
  108. // recorded lineage; the tool adds no authority of its own.
  109. ctx.subagents.interrupt(brandString<SessionId>(args.agent_id), { kind: 'ancestor', agent: caller })
  110. return Promise.resolve({ accepted: true })
  111. },
  112. }))
  113. }