background.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import {
  2. type PluginInput,
  3. type ToolDefinition,
  4. tool,
  5. } from '@opencode-ai/plugin';
  6. import { getDisabledAgents } from '../agents';
  7. import type { BackgroundTaskManager } from '../background';
  8. import type { PluginConfig } from '../config';
  9. import { SUBAGENT_NAMES } from '../config';
  10. import type { MultiplexerConfig } from '../config/schema';
  11. import { resolveRuntimeAgentName } from '../utils';
  12. const z = tool.schema;
  13. /**
  14. * Creates background task management tools for the plugin.
  15. * @param _ctx - Plugin input context
  16. * @param manager - Background task manager for launching and tracking tasks
  17. * @param _multiplexerConfig - Optional multiplexer configuration for session management
  18. * @param _pluginConfig - Optional plugin configuration for agent variants
  19. * @returns Object containing background_task, background_output, and background_cancel tools
  20. */
  21. export function createBackgroundTools(
  22. _ctx: PluginInput,
  23. manager: BackgroundTaskManager,
  24. _multiplexerConfig?: MultiplexerConfig,
  25. _pluginConfig?: PluginConfig,
  26. ): Record<string, ToolDefinition> {
  27. const disabled = getDisabledAgents(_pluginConfig);
  28. const agentNames = SUBAGENT_NAMES.filter((n) => !disabled.has(n)).join(', ');
  29. // Tool for launching agent tasks (fire-and-forget)
  30. const background_task = tool({
  31. description: `Launch background agent task. Returns task_id immediately.
  32. Flow: launch → wait for automatic notification when complete.
  33. Key behaviors:
  34. - Fire-and-forget: returns task_id in ~1ms
  35. - Parallel: up to 10 concurrent tasks
  36. - Auto-notify: parent session receives result when task completes`,
  37. args: {
  38. description: z
  39. .string()
  40. .describe('Short description of the task (5-10 words)'),
  41. prompt: z.string().describe('The task prompt for the agent'),
  42. agent: z.string().describe(`Agent to use: ${agentNames}`),
  43. },
  44. async execute(args, toolContext) {
  45. if (
  46. !toolContext ||
  47. typeof toolContext !== 'object' ||
  48. !('sessionID' in toolContext)
  49. ) {
  50. throw new Error('Invalid toolContext: missing sessionID');
  51. }
  52. const agent = resolveRuntimeAgentName(_pluginConfig, String(args.agent));
  53. const prompt = String(args.prompt);
  54. const description = String(args.description);
  55. const parentSessionId = (toolContext as { sessionID: string }).sessionID;
  56. // Validate agent against delegation rules
  57. if (!manager.isAgentAllowed(parentSessionId, agent)) {
  58. const allowed = manager.getAllowedSubagents(parentSessionId);
  59. return `Agent '${agent}' is not allowed. Allowed agents: ${allowed.join(', ')}`;
  60. }
  61. // Fire-and-forget launch
  62. const task = manager.launch({
  63. agent,
  64. prompt,
  65. description,
  66. parentSessionId,
  67. });
  68. return `Background task launched.
  69. Task ID: ${task.id}
  70. Agent: ${agent}
  71. Status: ${task.status}
  72. Use \`background_output\` with task_id="${task.id}" to get results.`;
  73. },
  74. });
  75. // Tool for retrieving output from background tasks
  76. const background_output = tool({
  77. description: `Get background task results after completion notification received.
  78. timeout=0: returns status immediately (no wait)
  79. timeout=N: waits up to N ms for completion
  80. Returns: results if completed, error if failed, status if running.`,
  81. args: {
  82. task_id: z.string().describe('Task ID from background_task'),
  83. timeout: z
  84. .number()
  85. .optional()
  86. .describe('Wait for completion (in ms, 0=no wait, default: 0)'),
  87. },
  88. async execute(args) {
  89. const taskId = String(args.task_id);
  90. const timeout =
  91. typeof args.timeout === 'number' && args.timeout > 0 ? args.timeout : 0;
  92. let task = manager.getResult(taskId);
  93. // Wait for completion if timeout specified
  94. if (
  95. task &&
  96. timeout > 0 &&
  97. task.status !== 'completed' &&
  98. task.status !== 'failed' &&
  99. task.status !== 'cancelled'
  100. ) {
  101. task = await manager.waitForCompletion(taskId, timeout);
  102. }
  103. if (!task) {
  104. return `Task not found: ${taskId}`;
  105. }
  106. // Calculate task duration
  107. const duration = task.completedAt
  108. ? `${Math.floor((task.completedAt.getTime() - task.startedAt.getTime()) / 1000)}s`
  109. : `${Math.floor((Date.now() - task.startedAt.getTime()) / 1000)}s`;
  110. let output = `Task: ${task.id}
  111. Description: ${task.description}
  112. Status: ${task.status}
  113. Duration: ${duration}
  114. ---
  115. `;
  116. // Include task result or error based on status
  117. if (task.status === 'completed' && task.result != null) {
  118. output += task.result;
  119. } else if (task.status === 'failed') {
  120. output += `Error: ${task.error}`;
  121. } else if (task.status === 'cancelled') {
  122. output += '(Task cancelled)';
  123. } else {
  124. output += '(Task still running)';
  125. }
  126. return output;
  127. },
  128. });
  129. // Tool for canceling running background tasks
  130. const background_cancel = tool({
  131. description: `Cancel background task(s).
  132. task_id: cancel specific task
  133. all=true: cancel all running tasks
  134. Only cancels pending/starting/running tasks.`,
  135. args: {
  136. task_id: z.string().optional().describe('Specific task to cancel'),
  137. all: z.boolean().optional().describe('Cancel all running tasks'),
  138. },
  139. async execute(args) {
  140. // Cancel all running tasks if requested
  141. if (args.all === true) {
  142. const count = manager.cancel();
  143. return `Cancelled ${count} task(s).`;
  144. }
  145. // Cancel specific task if task_id provided
  146. if (typeof args.task_id === 'string') {
  147. const count = manager.cancel(args.task_id);
  148. return count > 0
  149. ? `Cancelled task ${args.task_id}.`
  150. : `Task ${args.task_id} not found or not running.`;
  151. }
  152. return 'Specify task_id or use all=true.';
  153. },
  154. });
  155. return { background_task, background_output, background_cancel };
  156. }