wave-agent-sdk 0.16.8 → 0.16.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/constants/tools.d.ts +0 -1
  2. package/dist/constants/tools.d.ts.map +1 -1
  3. package/dist/constants/tools.js +0 -1
  4. package/dist/managers/aiManager.d.ts +0 -8
  5. package/dist/managers/aiManager.d.ts.map +1 -1
  6. package/dist/managers/aiManager.js +0 -45
  7. package/dist/managers/mcpManager.d.ts +5 -0
  8. package/dist/managers/mcpManager.d.ts.map +1 -1
  9. package/dist/managers/mcpManager.js +107 -12
  10. package/dist/managers/toolManager.d.ts +0 -6
  11. package/dist/managers/toolManager.d.ts.map +1 -1
  12. package/dist/managers/toolManager.js +1 -28
  13. package/dist/prompts/index.d.ts.map +1 -1
  14. package/dist/prompts/index.js +1 -12
  15. package/dist/services/authService.d.ts +10 -0
  16. package/dist/services/authService.d.ts.map +1 -1
  17. package/dist/services/authService.js +45 -0
  18. package/dist/services/configurationService.d.ts.map +1 -1
  19. package/dist/services/configurationService.js +8 -14
  20. package/dist/services/pluginLoader.d.ts.map +1 -1
  21. package/dist/services/pluginLoader.js +2 -2
  22. package/dist/telemetry/instrumentation.d.ts +5 -2
  23. package/dist/telemetry/instrumentation.d.ts.map +1 -1
  24. package/dist/telemetry/instrumentation.js +8 -4
  25. package/dist/tools/buildTool.d.ts +0 -2
  26. package/dist/tools/buildTool.d.ts.map +1 -1
  27. package/dist/tools/buildTool.js +0 -2
  28. package/dist/tools/cronCreateTool.d.ts.map +1 -1
  29. package/dist/tools/cronCreateTool.js +0 -1
  30. package/dist/tools/cronDeleteTool.d.ts.map +1 -1
  31. package/dist/tools/cronDeleteTool.js +0 -1
  32. package/dist/tools/cronListTool.d.ts.map +1 -1
  33. package/dist/tools/cronListTool.js +0 -1
  34. package/dist/tools/enterWorktreeTool.d.ts.map +1 -1
  35. package/dist/tools/enterWorktreeTool.js +0 -1
  36. package/dist/tools/exitWorktreeTool.d.ts.map +1 -1
  37. package/dist/tools/exitWorktreeTool.js +0 -1
  38. package/dist/tools/taskManagementTools.d.ts.map +1 -1
  39. package/dist/tools/taskManagementTools.js +0 -4
  40. package/dist/tools/types.d.ts +0 -15
  41. package/dist/tools/types.d.ts.map +1 -1
  42. package/dist/tools/webFetchTool.d.ts.map +1 -1
  43. package/dist/tools/webFetchTool.js +0 -1
  44. package/dist/types/mcp.d.ts +3 -1
  45. package/dist/types/mcp.d.ts.map +1 -1
  46. package/dist/utils/mcpUtils.d.ts.map +1 -1
  47. package/dist/utils/mcpUtils.js +0 -1
  48. package/package.json +1 -1
  49. package/src/constants/tools.ts +0 -1
  50. package/src/managers/aiManager.ts +0 -48
  51. package/src/managers/mcpManager.ts +122 -16
  52. package/src/managers/toolManager.ts +1 -32
  53. package/src/prompts/index.ts +0 -13
  54. package/src/services/authService.ts +56 -0
  55. package/src/services/configurationService.ts +8 -18
  56. package/src/services/pluginLoader.ts +2 -2
  57. package/src/telemetry/instrumentation.ts +12 -4
  58. package/src/tools/buildTool.ts +0 -4
  59. package/src/tools/cronCreateTool.ts +0 -1
  60. package/src/tools/cronDeleteTool.ts +0 -1
  61. package/src/tools/cronListTool.ts +0 -1
  62. package/src/tools/enterWorktreeTool.ts +0 -1
  63. package/src/tools/exitWorktreeTool.ts +0 -1
  64. package/src/tools/taskManagementTools.ts +0 -4
  65. package/src/tools/types.ts +0 -15
  66. package/src/tools/webFetchTool.ts +0 -1
  67. package/src/types/mcp.ts +8 -1
  68. package/src/utils/mcpUtils.ts +0 -1
  69. package/dist/tools/toolSearchTool.d.ts +0 -15
  70. package/dist/tools/toolSearchTool.d.ts.map +0 -1
  71. package/dist/tools/toolSearchTool.js +0 -200
  72. package/dist/utils/isDeferredTool.d.ts +0 -19
  73. package/dist/utils/isDeferredTool.d.ts.map +0 -1
  74. package/dist/utils/isDeferredTool.js +0 -31
  75. package/src/tools/toolSearchTool.ts +0 -245
  76. package/src/utils/isDeferredTool.ts +0 -36
@@ -45,8 +45,6 @@ import { logger } from "../utils/globalLogger.js";
45
45
 
46
46
  import type { SubagentConfiguration } from "../utils/subagentParser.js";
47
47
  import type { SkillMetadata } from "../types/skills.js";
48
- import { toolSearchTool } from "../tools/toolSearchTool.js";
49
- import { isDeferredTool } from "../utils/isDeferredTool.js";
50
48
  import { startToolSpan, endToolSpan } from "../telemetry/sessionTracing.js";
51
49
 
52
50
  export interface ToolManagerOptions {
@@ -134,7 +132,6 @@ class ToolManager {
134
132
  webFetchTool,
135
133
  enterWorktreeTool,
136
134
  exitWorktreeTool,
137
- toolSearchTool,
138
135
  ];
139
136
 
140
137
  for (const tool of builtInTools) {
@@ -210,7 +207,7 @@ class ToolManager {
210
207
  permissionMode: effectivePermissionMode,
211
208
  canUseToolCallback,
212
209
  permissionManager,
213
- toolManager: this, // Allow ToolSearchTool to access the tool manager
210
+ toolManager: this,
214
211
  taskManager:
215
212
  this.container.get<import("../services/taskManager.js").TaskManager>(
216
213
  "TaskManager",
@@ -349,13 +346,10 @@ class ToolManager {
349
346
  availableSkills?: SkillMetadata[];
350
347
  workdir?: string;
351
348
  isSubagent?: boolean;
352
- /** Set of discovered deferred tool names to include in the API call */
353
- discoveredTools?: Set<string>;
354
349
  }): ChatCompletionFunctionTool[] {
355
350
  const permissionManager =
356
351
  this.container.get<PermissionManager>("PermissionManager");
357
352
  const effectivePermissionMode = this.getPermissionMode();
358
- const discoveredTools = options?.discoveredTools;
359
353
  const builtInToolsConfig = Array.from(this.toolsRegistry.values())
360
354
  .filter((tool) => {
361
355
  // If tool is explicitly denied by name in permission rules, filter it out
@@ -380,10 +374,6 @@ class ToolManager {
380
374
  effectivePermissionMode !== "bypassPermissions"
381
375
  );
382
376
  }
383
- // Exclude deferred tools that haven't been discovered yet
384
- if (isDeferredTool(tool) && !discoveredTools?.has(tool.name)) {
385
- return false;
386
- }
387
377
  return true;
388
378
  })
389
379
  .map((tool) => {
@@ -406,10 +396,6 @@ class ToolManager {
406
396
  if (permissionManager?.isToolDenied(tool.function.name)) {
407
397
  return false;
408
398
  }
409
- // Exclude MCP tools that haven't been discovered yet
410
- if (discoveredTools && !discoveredTools.has(tool.function.name)) {
411
- return false;
412
- }
413
399
  return true;
414
400
  });
415
401
  return [...builtInToolsConfig, ...mcpToolsConfig];
@@ -453,23 +439,6 @@ class ToolManager {
453
439
  return this.container.get<PermissionManager>("PermissionManager");
454
440
  }
455
441
 
456
- /**
457
- * Get the names of all deferred tools (those that require ToolSearch to discover).
458
- */
459
- public getDeferredToolNames(): string[] {
460
- const permissionManager =
461
- this.container.get<PermissionManager>("PermissionManager");
462
- const builtInDeferred = Array.from(this.toolsRegistry.values())
463
- .filter((tool) => isDeferredTool(tool))
464
- .filter((tool) => !permissionManager?.isToolDenied(tool.name))
465
- .map((tool) => tool.name);
466
- const mcpDeferred = this.mcpManager
467
- .getMcpToolsConfig()
468
- .filter((tool) => !permissionManager?.isToolDenied(tool.function.name))
469
- .map((tool) => tool.function.name);
470
- return [...builtInDeferred, ...mcpDeferred];
471
- }
472
-
473
442
  /**
474
443
  * Get the task manager
475
444
  */
@@ -18,9 +18,7 @@ import {
18
18
  READ_TOOL_NAME,
19
19
  GLOB_TOOL_NAME,
20
20
  GREP_TOOL_NAME,
21
- TOOL_SEARCH_TOOL_NAME,
22
21
  } from "../constants/tools.js";
23
- import { isDeferredTool } from "../utils/isDeferredTool.js";
24
22
 
25
23
  export const BASE_SYSTEM_PROMPT = `You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.`;
26
24
 
@@ -260,17 +258,6 @@ export function buildSystemPrompt(
260
258
  prompt += `\n\n${TOOL_POLICY}`;
261
259
  }
262
260
 
263
- // List available deferred tool names with descriptions so the model knows they exist
264
- // and can decide which ones to discover via ToolSearch
265
- const deferredTools = tools.filter(isDeferredTool);
266
- if (deferredTools.length > 0) {
267
- const lines = deferredTools.map((t) => {
268
- const desc = t.config.function?.description;
269
- return desc ? `${t.name} - ${desc}` : t.name;
270
- });
271
- prompt += `\n\n<available-deferred-tools>\n${lines.join("\n")}\nThese tools are NOT loaded yet — call ${TOOL_SEARCH_TOOL_NAME} first to discover their schemas before invoking them.</available-deferred-tools>`;
272
- }
273
-
274
261
  prompt += `\n\n${OUTPUT_EFFICIENCY_PROMPT}`;
275
262
  prompt += `\n\n${TONE_AND_STYLE_PROMPT}`;
276
263
 
@@ -15,12 +15,16 @@ import {
15
15
  } from "fs";
16
16
  import * as path from "path";
17
17
  import * as os from "os";
18
+ import { randomBytes } from "crypto";
18
19
  import { createServer, Server } from "http";
19
20
  import { URL } from "url";
20
21
  import { execFile } from "child_process";
21
22
  import { promisify } from "util";
22
23
  import type { AuthConfig, AuthUser } from "../types/auth.js";
23
24
 
25
+ /** Persistent anonymous ID for telemetry fallback when SSO is not authenticated. */
26
+ let _anonymousId: string | undefined;
27
+
24
28
  const execFileAsync = promisify(execFile);
25
29
 
26
30
  export class AuthService {
@@ -312,3 +316,55 @@ export class AuthService {
312
316
  }
313
317
 
314
318
  export const authService = AuthService.getInstance();
319
+
320
+ /**
321
+ * Get or create a persistent anonymous ID for telemetry.
322
+ *
323
+ * Stored in ~/.wave/config.json as { anonymousId: "..." }.
324
+ * Generated once on first run (32-byte random hex) and reused thereafter.
325
+ * Falls back to an in-memory ID if file I/O fails.
326
+ */
327
+ export function getOrCreateAnonymousId(): string {
328
+ if (_anonymousId) return _anonymousId;
329
+
330
+ try {
331
+ const configPath = path.join(os.homedir(), ".wave", "config.json");
332
+ if (existsSync(configPath)) {
333
+ const content = readFileSync(configPath, "utf-8");
334
+ const config = JSON.parse(content) as { anonymousId?: string };
335
+ if (config.anonymousId) {
336
+ _anonymousId = config.anonymousId;
337
+ return _anonymousId;
338
+ }
339
+ }
340
+
341
+ // Generate and persist
342
+ _anonymousId = randomBytes(32).toString("hex");
343
+ const waveDir = path.dirname(configPath);
344
+ if (!existsSync(waveDir)) {
345
+ mkdirSync(waveDir, { recursive: true });
346
+ }
347
+ const existing = existsSync(configPath)
348
+ ? (JSON.parse(readFileSync(configPath, "utf-8")) as Record<
349
+ string,
350
+ unknown
351
+ >)
352
+ : {};
353
+ writeFileSync(
354
+ configPath,
355
+ JSON.stringify({ ...existing, anonymousId: _anonymousId }, null, 2),
356
+ "utf-8",
357
+ );
358
+ chmodSync(configPath, 0o600);
359
+ } catch {
360
+ // File I/O failed — use in-memory fallback
361
+ _anonymousId = randomBytes(32).toString("hex");
362
+ }
363
+
364
+ return _anonymousId;
365
+ }
366
+
367
+ /** @internal — reset anonymous ID cache for testing only */
368
+ export function __resetAnonymousIdForTesting(): void {
369
+ _anonymousId = undefined;
370
+ }
@@ -461,8 +461,7 @@ export class ConfigurationService {
461
461
  }
462
462
 
463
463
  // Resolve custom headers from environment: env (settings.json) > process.env
464
- const envCustomHeaders =
465
- process.env.WAVE_CUSTOM_HEADERS || process.env.WAVE_CUSTOM_HEADERS || "";
464
+ const envCustomHeaders = process.env.WAVE_CUSTOM_HEADERS || "";
466
465
  const parsedEnvHeaders = parseCustomHeaders(envCustomHeaders);
467
466
 
468
467
  // Merge headers: env headers < options < override
@@ -497,19 +496,13 @@ export class ConfigurationService {
497
496
  maxTokens?: number,
498
497
  permissionMode?: PermissionMode,
499
498
  ): ModelConfig {
500
- // Resolve agent model: override > options > env (settings.json) > process.env
499
+ // Resolve agent model: override > options > process.env (includes settings.json env)
501
500
  const resolvedAgentModel =
502
- model ||
503
- this.options.model ||
504
- process.env.WAVE_MODEL ||
505
- process.env.WAVE_MODEL;
501
+ model || this.options.model || process.env.WAVE_MODEL;
506
502
 
507
- // Resolve fast model: override > options > env (settings.json) > process.env
503
+ // Resolve fast model: override > options > process.env (includes settings.json env)
508
504
  const resolvedFastModel =
509
- fastModel ||
510
- this.options.fastModel ||
511
- process.env.WAVE_FAST_MODEL ||
512
- process.env.WAVE_FAST_MODEL;
505
+ fastModel || this.options.fastModel || process.env.WAVE_FAST_MODEL;
513
506
 
514
507
  // Validate required fields
515
508
  if (!resolvedAgentModel) {
@@ -572,8 +565,7 @@ export class ConfigurationService {
572
565
  }
573
566
 
574
567
  // Try env (settings.json) first, then process.env
575
- const envMaxInputTokens =
576
- process.env.WAVE_MAX_INPUT_TOKENS || process.env.WAVE_MAX_INPUT_TOKENS;
568
+ const envMaxInputTokens = process.env.WAVE_MAX_INPUT_TOKENS;
577
569
  if (envMaxInputTokens) {
578
570
  const parsed = parseInt(envMaxInputTokens, 10);
579
571
  if (!isNaN(parsed)) {
@@ -677,8 +669,7 @@ export class ConfigurationService {
677
669
  }
678
670
 
679
671
  // Try env (settings.json) first, then process.env
680
- const envMaxOutputTokens =
681
- process.env.WAVE_MAX_OUTPUT_TOKENS || process.env.WAVE_MAX_OUTPUT_TOKENS;
672
+ const envMaxOutputTokens = process.env.WAVE_MAX_OUTPUT_TOKENS;
682
673
  if (envMaxOutputTokens) {
683
674
  const parsed = parseInt(envMaxOutputTokens, 10);
684
675
  if (!isNaN(parsed) && parsed > 0) {
@@ -704,8 +695,7 @@ export class ConfigurationService {
704
695
  const models = new Set<string>();
705
696
 
706
697
  // Add current model from options or environment
707
- const currentModel =
708
- this.options.model || process.env.WAVE_MODEL || process.env.WAVE_MODEL;
698
+ const currentModel = this.options.model || process.env.WAVE_MODEL;
709
699
  if (currentModel) {
710
700
  models.add(currentModel);
711
701
  }
@@ -14,7 +14,6 @@ import {
14
14
  parseAgentFile,
15
15
  type SubagentConfiguration,
16
16
  } from "../utils/subagentParser.js";
17
- import { resolveMcpConfig } from "../managers/mcpManager.js";
18
17
  import { logger } from "../utils/globalLogger.js";
19
18
 
20
19
  export class PluginLoader {
@@ -143,7 +142,8 @@ export class PluginLoader {
143
142
  const mcpPath = path.join(pluginPath, ".mcp.json");
144
143
  try {
145
144
  const content = await fs.readFile(mcpPath, "utf-8");
146
- return resolveMcpConfig(JSON.parse(content)) as McpConfig;
145
+ // Return raw config — let McpManager resolve templates and capture originalUrl
146
+ return JSON.parse(content) as McpConfig;
147
147
  } catch {
148
148
  return undefined;
149
149
  }
@@ -18,7 +18,10 @@ import type {
18
18
  LogRecordExporter,
19
19
  ReadableLogRecord,
20
20
  } from "@opentelemetry/sdk-logs";
21
- import { AuthService } from "../services/authService.js";
21
+ import {
22
+ AuthService,
23
+ getOrCreateAnonymousId,
24
+ } from "../services/authService.js";
22
25
 
23
26
  // Lazy-loaded OTEL modules — only imported when telemetry is initialized
24
27
  let sdkNode: typeof import("@opentelemetry/sdk-node") | undefined;
@@ -471,8 +474,11 @@ export function isInitialized(): boolean {
471
474
  export { JsonlSpanExporter, JsonlLogExporter };
472
475
 
473
476
  /**
474
- * Get telemetry attributes based on the authenticated SSO user.
475
- * Returns user.id and user.email when SSO authenticated, empty object otherwise.
477
+ * Get telemetry attributes for the current session.
478
+ *
479
+ * Priority:
480
+ * 1. SSO authenticated → server-provided user.id + user.email
481
+ * 2. Not authenticated → persistent anonymous ID from ~/.wave/config.json
476
482
  */
477
483
  export function getTelemetryAttributes(): Record<string, string> {
478
484
  try {
@@ -487,5 +493,7 @@ export function getTelemetryAttributes(): Record<string, string> {
487
493
  } catch {
488
494
  // AuthService not available or not authenticated
489
495
  }
490
- return {};
496
+
497
+ // Fallback to anonymous ID
498
+ return { "user.id": getOrCreateAnonymousId() };
491
499
  }
@@ -24,8 +24,6 @@ export interface ToolDef {
24
24
  params: Record<string, unknown>,
25
25
  context: ToolContext,
26
26
  ) => string;
27
- shouldDefer?: boolean;
28
- alwaysLoad?: boolean;
29
27
  additionalProperties?: boolean;
30
28
  }
31
29
 
@@ -59,7 +57,5 @@ export function buildTool(def: ToolDef): ToolPlugin {
59
57
  execute: def.execute,
60
58
  prompt: promptFn,
61
59
  formatCompactParams: def.formatCompactParams,
62
- shouldDefer: def.shouldDefer ?? false,
63
- alwaysLoad: def.alwaysLoad ?? false,
64
60
  };
65
61
  }
@@ -48,7 +48,6 @@ Returns a job ID you can pass to CronDelete.`;
48
48
 
49
49
  export const cronCreateTool: ToolPlugin = {
50
50
  name: CRON_CREATE_TOOL_NAME,
51
- shouldDefer: true,
52
51
  config: {
53
52
  type: "function",
54
53
  function: {
@@ -7,7 +7,6 @@ const CRON_DELETE_PROMPT = `Cancel a cron job previously scheduled with CronCrea
7
7
 
8
8
  export const cronDeleteTool: ToolPlugin = {
9
9
  name: CRON_DELETE_TOOL_NAME,
10
- shouldDefer: true,
11
10
  config: {
12
11
  type: "function",
13
12
  function: {
@@ -7,7 +7,6 @@ const CRON_LIST_PROMPT = `List all cron jobs scheduled via CronCreate in this se
7
7
 
8
8
  export const cronListTool: ToolPlugin = {
9
9
  name: CRON_LIST_TOOL_NAME,
10
- shouldDefer: true,
11
10
  config: {
12
11
  type: "function",
13
12
  function: {
@@ -48,7 +48,6 @@ export const ENTER_WORKTREE_TOOL_PROMPT = `Use this tool ONLY when the user expl
48
48
 
49
49
  export const enterWorktreeTool: ToolPlugin = {
50
50
  name: ENTER_WORKTREE_TOOL_NAME,
51
- shouldDefer: true,
52
51
  config: {
53
52
  type: "function",
54
53
  function: {
@@ -47,7 +47,6 @@ If called outside an EnterWorktree session, the tool is a **no-op**: it reports
47
47
 
48
48
  export const exitWorktreeTool: ToolPlugin = {
49
49
  name: EXIT_WORKTREE_TOOL_NAME,
50
- shouldDefer: true,
51
50
  config: {
52
51
  type: "function",
53
52
  function: {
@@ -9,7 +9,6 @@ import {
9
9
 
10
10
  export const taskCreateTool: ToolPlugin = {
11
11
  name: TASK_CREATE_TOOL_NAME,
12
- shouldDefer: true,
13
12
  config: {
14
13
  type: "function",
15
14
  function: {
@@ -144,7 +143,6 @@ NOTE that you should not use this tool if there is only one trivial task to do.
144
143
 
145
144
  export const taskGetTool: ToolPlugin = {
146
145
  name: TASK_GET_TOOL_NAME,
147
- shouldDefer: true,
148
146
  config: {
149
147
  type: "function",
150
148
  function: {
@@ -204,7 +202,6 @@ Returns full task details:
204
202
 
205
203
  export const taskUpdateTool: ToolPlugin = {
206
204
  name: TASK_UPDATE_TOOL_NAME,
207
- shouldDefer: true,
208
205
  config: {
209
206
  type: "function",
210
207
  function: {
@@ -559,7 +556,6 @@ Set up task dependencies:
559
556
 
560
557
  export const taskListTool: ToolPlugin = {
561
558
  name: TASK_LIST_TOOL_NAME,
562
- shouldDefer: true,
563
559
  config: {
564
560
  type: "function",
565
561
  function: {
@@ -31,21 +31,6 @@ export interface ToolPlugin {
31
31
  workdir?: string;
32
32
  isSubagent?: boolean;
33
33
  }) => string;
34
- /**
35
- * When true, this tool is deferred — it's not sent to the API until the model
36
- * discovers it via ToolSearch. MCP tools are always deferred.
37
- */
38
- shouldDefer?: boolean;
39
- /**
40
- * When true, this tool is never deferred — its full schema always appears in
41
- * the initial prompt even when tool search is enabled.
42
- */
43
- alwaysLoad?: boolean;
44
- /**
45
- * When true, this is an MCP tool (auto-set by McpManager). MCP tools are
46
- * always deferred unless they have alwaysLoad: true.
47
- */
48
- isMcp?: boolean;
49
34
  }
50
35
 
51
36
  export interface ToolResult {
@@ -103,7 +103,6 @@ const GITHUB_URL_ERROR =
103
103
 
104
104
  export const webFetchTool: ToolPlugin = {
105
105
  name: WEB_FETCH_TOOL_NAME,
106
- shouldDefer: true,
107
106
  config: {
108
107
  type: "function",
109
108
  function: {
package/src/types/mcp.ts CHANGED
@@ -26,7 +26,14 @@ export interface McpTool {
26
26
  export interface McpServerStatus {
27
27
  name: string;
28
28
  config: McpServerConfig;
29
- status: "disconnected" | "connected" | "connecting" | "error";
29
+ /** Pre-resolution URL with template variables (e.g. ${WAVE_SSO_TOKEN}) preserved for safe display */
30
+ originalUrl?: string;
31
+ status:
32
+ | "disconnected"
33
+ | "connected"
34
+ | "connecting"
35
+ | "reconnecting"
36
+ | "error";
30
37
  tools?: McpTool[];
31
38
  toolCount?: number;
32
39
  capabilities?: string[];
@@ -91,7 +91,6 @@ export function createMcpToolPlugin(
91
91
  return {
92
92
  name: prefixedName,
93
93
  config: mcpToolToOpenAITool(mcpTool, serverName),
94
- isMcp: true, // MCP tools are always deferred
95
94
  async execute(
96
95
  args: Record<string, unknown>,
97
96
  context?: ToolContext,
@@ -1,15 +0,0 @@
1
- /**
2
- * ToolSearchTool - Discovers deferred tool schemas on demand.
3
- *
4
- * When tool deferral is enabled, deferred tools are not sent to the API.
5
- * The model must call this tool to discover a deferred tool's full schema
6
- * before it can invoke it.
7
- *
8
- * Query formats:
9
- * - "select:ToolName" — direct selection by name (comma-separated for multiple)
10
- * - "notebook jupyter" — keyword search, up to max_results best matches
11
- * - "+slack send" — require "slack" in the name, rank by remaining terms
12
- */
13
- import { ToolPlugin } from "./types.js";
14
- export declare const toolSearchTool: ToolPlugin;
15
- //# sourceMappingURL=toolSearchTool.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"toolSearchTool.d.ts","sourceRoot":"","sources":["../../src/tools/toolSearchTool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAC;AA4GjE,eAAO,MAAM,cAAc,EAAE,UAuH5B,CAAC"}
@@ -1,200 +0,0 @@
1
- /**
2
- * ToolSearchTool - Discovers deferred tool schemas on demand.
3
- *
4
- * When tool deferral is enabled, deferred tools are not sent to the API.
5
- * The model must call this tool to discover a deferred tool's full schema
6
- * before it can invoke it.
7
- *
8
- * Query formats:
9
- * - "select:ToolName" — direct selection by name (comma-separated for multiple)
10
- * - "notebook jupyter" — keyword search, up to max_results best matches
11
- * - "+slack send" — require "slack" in the name, rank by remaining terms
12
- */
13
- import { isDeferredTool, TOOL_SEARCH_TOOL_NAME, } from "../utils/isDeferredTool.js";
14
- function formatSchema(tool) {
15
- const desc = tool.config.function.description || "";
16
- const params = JSON.stringify(tool.config.function.parameters || {}, null, 2);
17
- return `${tool.name}: ${desc}\nParameters: ${params}`;
18
- }
19
- /**
20
- * Parse tool name into searchable parts (handles CamelCase and underscores).
21
- */
22
- function parseToolName(name) {
23
- return name
24
- .replace(/([a-z])([A-Z])/g, "$1 $2") // CamelCase to spaces
25
- .replace(/_/g, " ")
26
- .toLowerCase()
27
- .split(/\s+/)
28
- .filter(Boolean);
29
- }
30
- /**
31
- * Keyword search over deferred tools by name and description.
32
- * Matches Claude Code's scoring: required terms (+prefix) must all match,
33
- * optional terms contribute to ranking.
34
- */
35
- function keywordSearch(query, deferredTools, maxResults) {
36
- const queryLower = query.toLowerCase().trim();
37
- const queryTerms = queryLower.split(/\s+/).filter(Boolean);
38
- // Exact match fast path
39
- const exact = deferredTools.find((t) => t.name.toLowerCase() === queryLower);
40
- if (exact)
41
- return [exact];
42
- // Partition into required (+prefixed) and optional terms
43
- const requiredTerms = [];
44
- const optionalTerms = [];
45
- for (const term of queryTerms) {
46
- if (term.startsWith("+") && term.length > 1) {
47
- requiredTerms.push(term.slice(1));
48
- }
49
- else {
50
- optionalTerms.push(term);
51
- }
52
- }
53
- const allScoringTerms = requiredTerms.length > 0
54
- ? [...requiredTerms, ...optionalTerms]
55
- : queryTerms;
56
- // Pre-filter to tools matching ALL required terms
57
- let candidateTools = deferredTools;
58
- if (requiredTerms.length > 0) {
59
- candidateTools = deferredTools.filter((tool) => {
60
- const parts = parseToolName(tool.name);
61
- const desc = (tool.config.function.description || "").toLowerCase();
62
- return requiredTerms.every((term) => parts.includes(term) ||
63
- parts.some((p) => p.includes(term)) ||
64
- desc.includes(term));
65
- });
66
- }
67
- // Score each tool
68
- const scored = candidateTools
69
- .map((tool) => {
70
- const parts = parseToolName(tool.name);
71
- const desc = (tool.config.function.description || "").toLowerCase();
72
- let score = 0;
73
- for (const term of allScoringTerms) {
74
- // Exact part match (high weight)
75
- if (parts.includes(term)) {
76
- score += tool.isMcp ? 12 : 10;
77
- }
78
- else if (parts.some((p) => p.includes(term))) {
79
- score += tool.isMcp ? 6 : 5;
80
- }
81
- // Full name fallback
82
- if (tool.name.toLowerCase().includes(term) && score === 0) {
83
- score += 3;
84
- }
85
- // Description match
86
- if (desc.includes(term)) {
87
- score += 2;
88
- }
89
- }
90
- return { tool, score };
91
- })
92
- .filter((s) => s.score > 0)
93
- .sort((a, b) => b.score - a.score)
94
- .slice(0, maxResults)
95
- .map((s) => s.tool);
96
- return scored;
97
- }
98
- export const toolSearchTool = {
99
- name: TOOL_SEARCH_TOOL_NAME,
100
- config: {
101
- type: "function",
102
- function: {
103
- name: TOOL_SEARCH_TOOL_NAME,
104
- description: `Fetches full schema definitions for deferred tools so they can be called.
105
-
106
- Deferred tools appear by name and description in <available-deferred-tools> messages. The full parameter schema is NOT loaded yet — use this tool to fetch it before invoking a deferred tool. This tool takes a query, matches it against the deferred tool list, and returns the matched tools' complete JSONSchema definitions inside a <functions> block. Once a tool's schema appears in that result, it is callable exactly like any tool defined at the top of the prompt.
107
-
108
- Result format: each matched tool appears as one <function>{"description": "...", "name": "...", "parameters": {...}}`,
109
- parameters: {
110
- type: "object",
111
- properties: {
112
- query: {
113
- type: "string",
114
- description: 'Search query for finding deferred tools. Supports: "select:ToolName" for direct lookup, or keyword search like "notebook jupyter". Use "+term" to require a term (e.g. "+slack send").',
115
- },
116
- max_results: {
117
- type: "number",
118
- description: "Maximum number of results to return for keyword search (default: 5).",
119
- },
120
- },
121
- required: ["query"],
122
- additionalProperties: false,
123
- },
124
- },
125
- },
126
- shouldDefer: false, // Always available
127
- execute: async (args, context) => {
128
- const { query, max_results = 5 } = args;
129
- if (!query) {
130
- return {
131
- success: false,
132
- content: "",
133
- error: "Missing required 'query' parameter",
134
- };
135
- }
136
- if (!context.toolManager) {
137
- return {
138
- success: false,
139
- content: "",
140
- error: "ToolManager not available in context",
141
- };
142
- }
143
- const allTools = context.toolManager.list();
144
- const deferredTools = allTools.filter(isDeferredTool);
145
- // Handle select: prefix
146
- const selectMatch = query.match(/^select:(.+)$/i);
147
- if (selectMatch) {
148
- const requested = selectMatch[1]
149
- .split(",")
150
- .map((s) => s.trim())
151
- .filter(Boolean);
152
- const found = [];
153
- const missing = [];
154
- for (const toolName of requested) {
155
- const tool = deferredTools.find((t) => t.name === toolName) ??
156
- allTools.find((t) => t.name === toolName);
157
- if (tool) {
158
- if (!found.some((f) => f.name === tool.name))
159
- found.push(tool);
160
- }
161
- else {
162
- missing.push(toolName);
163
- }
164
- }
165
- if (found.length === 0) {
166
- return {
167
- success: false,
168
- content: "",
169
- error: `No matching deferred tools found for: ${missing.join(", ")}`,
170
- };
171
- }
172
- const result = found.map(formatSchema).join("\n\n---\n\n");
173
- const shortResult = `Discovered tools: ${found.map((t) => t.name).join(", ")}`;
174
- return {
175
- success: true,
176
- content: result,
177
- shortResult,
178
- };
179
- }
180
- // Keyword search
181
- const matches = keywordSearch(query, deferredTools, max_results);
182
- if (matches.length === 0) {
183
- return {
184
- success: false,
185
- content: "",
186
- error: `No matching deferred tools found for query: "${query}". Available deferred tools: ${getDeferredToolNamesList(deferredTools)}`,
187
- };
188
- }
189
- const result = matches.map(formatSchema).join("\n\n---\n\n");
190
- const shortResult = `Found ${matches.length} tools: ${matches.map((t) => t.name).join(", ")}`;
191
- return {
192
- success: true,
193
- content: result,
194
- shortResult,
195
- };
196
- },
197
- };
198
- function getDeferredToolNamesList(tools) {
199
- return tools.map((t) => t.name).join(", ");
200
- }