summaryrefslogtreecommitdiffhomepage
path: root/packages/session-orchestrator/src/tools-filter.ts
blob: 28e82bfc2fb060e3c0d61fc025b9483ab56e8d96 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import { defineFilter, type FilterDescriptor, type ToolContract } from "@dispatch/kernel";

/** Per-turn tool-assembly value threaded through the `tools` filter chain. */
export interface ToolAssembly {
  /** The tool set resolved for this turn (the value filters transform). */
  readonly tools: readonly ToolContract[];
  /** This turn's working directory (verbatim from the request), for cwd-aware filters. */
  readonly cwd?: string;
  /**
   * The computer this turn executes on (SSH alias), for computer-aware
   * filters. Omitted/`undefined` = LOCAL (today's behavior). When set, the
   * turn is REMOTE — {@link filterRemoteIncompatibleTools} drops tools that
   * cannot run over SFTP (local-process servers). Mirrors `cwd?`.
   */
  readonly computerId?: string;
  /** The conversation this turn belongs to. */
  readonly conversationId: string;
}

/** Filter chain run once per turn to transform the tool set before it reaches runTurn. */
export const toolsFilter: FilterDescriptor<ToolAssembly> = defineFilter<ToolAssembly>(
  "session-orchestrator/tools",
);

/**
 * Remote-degradation rule (plan §6). When a turn is REMOTE
 * (`assembly.computerId !== undefined`), drop tools that cannot execute over
 * SFTP because they spawn local processes:
 *
 * - the tool named exactly `"lsp"` — its servers are local LSP processes that
 *   can't see remote files over SFTP, AND
 * - any tool whose name includes `"__"` — MCP-namespaced tools
 *   (`<serverId>__<toolName>`); MCP servers spawn local processes.
 *
 * When `assembly.computerId` is `undefined` (LOCAL), this is a passthrough —
 * nothing is dropped (byte-identical to today). Tool-name matching is DATA (the
 * kernel routes tool-calls by name — that is the sanctioned string-keyed
 * exception), so a name-based filter is correct here, not a string-keyed
 * cross-feature code lookup.
 *
 * Extracted from the filter handler so it is unit-testable without I/O. Mirrors
 * MCP's `filterMcpTools` extraction pattern.
 */
export function filterRemoteIncompatibleTools(assembly: ToolAssembly): ToolAssembly {
  // LOCAL — passthrough, byte-identical to today.
  if (assembly.computerId === undefined) return assembly;
  // REMOTE — drop lsp + MCP-namespaced tools (local-process servers).
  const filtered = assembly.tools.filter((tool) => {
    if (tool.name === "lsp") return false;
    if (tool.name.includes("__")) return false;
    return true;
  });
  return {
    tools: filtered,
    ...(assembly.cwd !== undefined ? { cwd: assembly.cwd } : {}),
    ...(assembly.computerId !== undefined ? { computerId: assembly.computerId } : {}),
    conversationId: assembly.conversationId,
  };
}