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