toolPool.ts 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. import { feature } from 'bun:bundle'
  2. import partition from 'lodash-es/partition.js'
  3. import uniqBy from 'lodash-es/uniqBy.js'
  4. import { COORDINATOR_MODE_ALLOWED_TOOLS } from '../constants/tools.js'
  5. import { isMcpTool } from '../services/mcp/utils.js'
  6. import type { Tool, ToolPermissionContext, Tools } from '../Tool.js'
  7. // MCP tool name suffixes for PR activity subscription. These are lightweight
  8. // orchestration actions the coordinator calls directly rather than delegating
  9. // to workers. Matched by suffix since the MCP server name prefix may vary.
  10. const PR_ACTIVITY_TOOL_SUFFIXES = [
  11. 'subscribe_pr_activity',
  12. 'unsubscribe_pr_activity',
  13. ]
  14. export function isPrActivitySubscriptionTool(name: string): boolean {
  15. return PR_ACTIVITY_TOOL_SUFFIXES.some(suffix => name.endsWith(suffix))
  16. }
  17. // Dead code elimination: conditional imports for feature-gated modules
  18. /* eslint-disable @typescript-eslint/no-require-imports */
  19. const coordinatorModeModule = feature('COORDINATOR_MODE')
  20. ? (require('../coordinator/coordinatorMode.js') as typeof import('../coordinator/coordinatorMode.js'))
  21. : null
  22. /* eslint-enable @typescript-eslint/no-require-imports */
  23. /**
  24. * Filters a tool array to the set allowed in coordinator mode.
  25. * Shared between the REPL path (mergeAndFilterTools) and the headless
  26. * path (main.tsx) so both stay in sync.
  27. *
  28. * PR activity subscription tools are always allowed since subscription
  29. * management is orchestration.
  30. */
  31. export function applyCoordinatorToolFilter(tools: Tools): Tools {
  32. return tools.filter(
  33. t =>
  34. COORDINATOR_MODE_ALLOWED_TOOLS.has(t.name) ||
  35. isPrActivitySubscriptionTool(t.name),
  36. )
  37. }
  38. /**
  39. * Pure function that merges tool pools and applies coordinator mode filtering.
  40. *
  41. * Lives in a React-free file so print.ts can import it without pulling
  42. * react/ink into the SDK module graph. The useMergedTools hook delegates
  43. * to this function inside useMemo.
  44. *
  45. * @param initialTools - Extra tools to include (built-in + startup MCP from props).
  46. * @param assembled - Tools from assembleToolPool (built-in + MCP, deduped).
  47. * @param mode - The permission context mode.
  48. * @returns Merged, deduplicated, and coordinator-filtered tool array.
  49. */
  50. export function mergeAndFilterTools(
  51. initialTools: Tools,
  52. assembled: Tools,
  53. mode: ToolPermissionContext['mode'],
  54. ): Tools {
  55. // Merge initialTools on top - they take precedence in deduplication.
  56. // initialTools may include built-in tools (from getTools() in REPL.tsx) which
  57. // overlap with assembled tools. uniqBy handles this deduplication.
  58. // Partition-sort for prompt-cache stability (same as assembleToolPool):
  59. // built-ins must stay a contiguous prefix for the server's cache policy.
  60. const [mcp, builtIn] = partition(
  61. uniqBy([...initialTools, ...assembled], 'name'),
  62. isMcpTool,
  63. )
  64. const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
  65. const tools = [...builtIn.sort(byName), ...mcp.sort(byName)]
  66. if (feature('COORDINATOR_MODE') && coordinatorModeModule) {
  67. if (coordinatorModeModule.isCoordinatorMode()) {
  68. return applyCoordinatorToolFilter(tools)
  69. }
  70. }
  71. return tools
  72. }