toolnexus 0.9.3 → 0.9.5

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.
package/dist/client.d.ts CHANGED
@@ -44,6 +44,24 @@ export interface ClientOptions {
44
44
  /** §8 Gap 2. HTTP transport for LLM requests (retries included). Omit ⇒ global fetch. Scope is the
45
45
  * LLM path only; MCP transports use the SDK's own client. */
46
46
  fetch?: typeof fetch;
47
+ /** §8 Resilience. Classify each LLM-call failure into "retry" or "fail". Omit ⇒ the default
48
+ * classifier (retryable status/network within budget ⇒ retry, else fail) — byte-identical to
49
+ * today. A "retry" is always bounded by `retries`; the classifier cannot loop unbounded.
50
+ * There is no "suspend" tier — suspension (§10) stays a user-action pause, not an error path. */
51
+ onError?: (info: ErrorInfo) => ErrorTier;
52
+ }
53
+ /** §8 Resilience. What to do with a failed LLM call. */
54
+ export type ErrorTier = "retry" | "fail";
55
+ /** §8 Resilience. Context passed to `onError` for each failed LLM attempt. */
56
+ export interface ErrorInfo {
57
+ /** The thrown network/transport error, when the failure was not an HTTP response. */
58
+ error?: unknown;
59
+ /** The HTTP status, when the failure was a non-ok response. */
60
+ status?: number;
61
+ /** Zero-based attempt index (0 = first try). */
62
+ attempt: number;
63
+ /** Whether `status`/the error is in the default retryable set (429/5xx/network). */
64
+ retryable: boolean;
47
65
  }
48
66
  /**
49
67
  * Semantic observability events (§8). NOT counter/histogram primitives — readable records the
@@ -263,7 +281,8 @@ export declare class Client {
263
281
  private system;
264
282
  /** Build a run-scoped abort signal from the optional run-level timeout + an external signal. */
265
283
  private makeSignal;
266
- /** fetch with retry + exponential backoff on 429/5xx/network, honoring Retry-After; aborts via signal. */
284
+ /** fetch with retry + exponential backoff on 429/5xx/network, honoring Retry-After; aborts via signal.
285
+ * The retry-vs-fail decision is the host's `onError` (default: retryable-within-budget ⇒ retry). */
267
286
  private llmFetch;
268
287
  /** Run one tool through beforeTool/afterTool hooks (mutate args, short-circuit, transform result). */
269
288
  private runTool;
package/dist/client.js CHANGED
@@ -291,16 +291,24 @@ export class Client {
291
291
  }
292
292
  return ctrl.signal;
293
293
  }
294
- /** fetch with retry + exponential backoff on 429/5xx/network, honoring Retry-After; aborts via signal. */
294
+ /** fetch with retry + exponential backoff on 429/5xx/network, honoring Retry-After; aborts via signal.
295
+ * The retry-vs-fail decision is the host's `onError` (default: retryable-within-budget ⇒ retry). */
295
296
  async llmFetch(url, init, signal) {
296
297
  const retries = this.opts.retries ?? 2;
297
298
  const base = this.opts.retryBaseMs ?? 500;
299
+ // Default classifier = today's behavior, expressed as an onError. A "retry" is always
300
+ // capped by the budget below (attempt === retries stops regardless of the classifier).
301
+ const classify = this.opts.onError ?? ((info) => (info.retryable ? "retry" : "fail"));
298
302
  let lastErr;
299
303
  for (let attempt = 0; attempt <= retries; attempt++) {
300
304
  try {
301
305
  const res = await (this.opts.fetch ?? fetch)(url, { ...init, signal });
302
- if (res.ok || !RETRYABLE.has(res.status) || attempt === retries)
306
+ if (res.ok)
303
307
  return res;
308
+ const retryable = RETRYABLE.has(res.status);
309
+ const tier = classify({ status: res.status, attempt, retryable });
310
+ if (tier === "fail" || attempt === retries)
311
+ return res; // caller surfaces the non-ok status
304
312
  const ra = Number(res.headers.get("retry-after"));
305
313
  await delay(ra ? ra * 1000 : base * 2 ** attempt + Math.random() * 100, signal);
306
314
  }
@@ -308,7 +316,8 @@ export class Client {
308
316
  if (signal.aborted)
309
317
  throw e; // abort/timeout: don't retry
310
318
  lastErr = e;
311
- if (attempt === retries)
319
+ const tier = classify({ error: e, attempt, retryable: true });
320
+ if (tier === "fail" || attempt === retries)
312
321
  throw e;
313
322
  await delay(base * 2 ** attempt + Math.random() * 100, signal);
314
323
  }
package/dist/toolkit.d.ts CHANGED
@@ -18,6 +18,13 @@ export interface ToolkitOptions {
18
18
  skillProvider?: () => SkillDef[] | Promise<SkillDef[]>;
19
19
  /** Per-agent skill allowlist keyed on name; same semantics as the MCP tools filter (§3, S2). */
20
20
  skillsFilter?: Record<string, boolean>;
21
+ /** Drop tools by their FINAL exposed name across every source (MCP `server_tool`, builtins,
22
+ * native, a2a, extras), applied after aggregation — the simple "just turn these off" list.
23
+ * Empty/undefined ⇒ nothing disabled. Composes with builtins/skillsFilter/server allowlists. */
24
+ disableTools?: string[];
25
+ /** Drop skills by name from the `skill` catalog — sugar over a drop-list in `skillsFilter`
26
+ * (each name folded in as `false`, without overriding an explicit filter entry). */
27
+ disableSkills?: string[];
21
28
  /** Sibling-file sample cap: 0 ⇒ default 10, n>0 ⇒ cap, -1 ⇒ omit <skill_files> (§3, S5). */
22
29
  skillSampleLimit?: number;
23
30
  extraTools?: Tool[];
package/dist/toolkit.js CHANGED
@@ -14,7 +14,9 @@ export class Toolkit {
14
14
  /** A2A profile read from a top-level `a2a` config block (serve() falls back to this). */
15
15
  a2aConfig,
16
16
  /** MCP serve profile read from a top-level `mcpServer` config block. */
17
- mcpServerConfig) {
17
+ mcpServerConfig,
18
+ /** Final-name drop-list (§4A) applied across every source after aggregation. */
19
+ disableTools = []) {
18
20
  this.mcp = mcp;
19
21
  this.skill = skill;
20
22
  this.a2aConfig = a2aConfig;
@@ -24,8 +26,11 @@ export class Toolkit {
24
26
  // front, then apply the normal first-wins dedupe for MCP/skill/agents/extras.
25
27
  const extraNames = new Set(extraTools.map((t) => t.name));
26
28
  const activeBuiltins = builtins.filter((b) => !extraNames.has(b.name));
29
+ const disabled = new Set(disableTools);
27
30
  const all = [...(mcp?.tools ?? []), ...(skill ? [skill.tool] : []), ...activeBuiltins, ...agents, ...extraTools];
28
31
  for (const t of all) {
32
+ if (disabled.has(t.name))
33
+ continue; // §4A drop-list by final exposed name
29
34
  if (this.byName.has(t.name)) {
30
35
  console.warn(`[toolnexus] duplicate tool name "${t.name}" — keeping first`);
31
36
  continue;
@@ -51,10 +56,15 @@ export class Toolkit {
51
56
  }
52
57
  }
53
58
  const dataDefs = [...(opts.skills ?? []), ...providerDefs];
59
+ // disableSkills is sugar over a skillsFilter drop-list: fold each name in as false
60
+ // WITHOUT overriding an explicit skillsFilter entry (explicit entries win).
61
+ const filter = opts.disableSkills && opts.disableSkills.length > 0
62
+ ? { ...Object.fromEntries(opts.disableSkills.map((n) => [n, false])), ...opts.skillsFilter }
63
+ : opts.skillsFilter;
54
64
  skill = loadSkills({
55
65
  dirs: opts.skillsDir,
56
66
  skills: dataDefs.length > 0 ? dataDefs : undefined,
57
- filter: opts.skillsFilter,
67
+ filter,
58
68
  sampleLimit: opts.skillSampleLimit,
59
69
  });
60
70
  }
@@ -93,7 +103,7 @@ export class Toolkit {
93
103
  if (opts.mcpConfig && typeof opts.mcpConfig === "object" && "mcpServer" in opts.mcpConfig) {
94
104
  mcpServerConfig = opts.mcpConfig.mcpServer;
95
105
  }
96
- return new Toolkit(mcp, skill, builtins, agentToolsFlat, opts.extraTools ?? [], a2aConfig, mcpServerConfig);
106
+ return new Toolkit(mcp, skill, builtins, agentToolsFlat, opts.extraTools ?? [], a2aConfig, mcpServerConfig, opts.disableTools ?? []);
97
107
  }
98
108
  tools() {
99
109
  return [...this.byName.values()];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolnexus",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "Dynamic MCP servers + agent skills as uniform tools for any LLM (opencode-style).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/client.ts CHANGED
@@ -47,6 +47,26 @@ export interface ClientOptions {
47
47
  /** §8 Gap 2. HTTP transport for LLM requests (retries included). Omit ⇒ global fetch. Scope is the
48
48
  * LLM path only; MCP transports use the SDK's own client. */
49
49
  fetch?: typeof fetch
50
+ /** §8 Resilience. Classify each LLM-call failure into "retry" or "fail". Omit ⇒ the default
51
+ * classifier (retryable status/network within budget ⇒ retry, else fail) — byte-identical to
52
+ * today. A "retry" is always bounded by `retries`; the classifier cannot loop unbounded.
53
+ * There is no "suspend" tier — suspension (§10) stays a user-action pause, not an error path. */
54
+ onError?: (info: ErrorInfo) => ErrorTier
55
+ }
56
+
57
+ /** §8 Resilience. What to do with a failed LLM call. */
58
+ export type ErrorTier = "retry" | "fail"
59
+
60
+ /** §8 Resilience. Context passed to `onError` for each failed LLM attempt. */
61
+ export interface ErrorInfo {
62
+ /** The thrown network/transport error, when the failure was not an HTTP response. */
63
+ error?: unknown
64
+ /** The HTTP status, when the failure was a non-ok response. */
65
+ status?: number
66
+ /** Zero-based attempt index (0 = first try). */
67
+ attempt: number
68
+ /** Whether `status`/the error is in the default retryable set (429/5xx/network). */
69
+ retryable: boolean
50
70
  }
51
71
 
52
72
  /**
@@ -439,21 +459,29 @@ export class Client {
439
459
  return ctrl.signal
440
460
  }
441
461
 
442
- /** fetch with retry + exponential backoff on 429/5xx/network, honoring Retry-After; aborts via signal. */
462
+ /** fetch with retry + exponential backoff on 429/5xx/network, honoring Retry-After; aborts via signal.
463
+ * The retry-vs-fail decision is the host's `onError` (default: retryable-within-budget ⇒ retry). */
443
464
  private async llmFetch(url: string, init: RequestInit, signal: AbortSignal): Promise<Response> {
444
465
  const retries = this.opts.retries ?? 2
445
466
  const base = this.opts.retryBaseMs ?? 500
467
+ // Default classifier = today's behavior, expressed as an onError. A "retry" is always
468
+ // capped by the budget below (attempt === retries stops regardless of the classifier).
469
+ const classify = this.opts.onError ?? ((info: ErrorInfo): ErrorTier => (info.retryable ? "retry" : "fail"))
446
470
  let lastErr: unknown
447
471
  for (let attempt = 0; attempt <= retries; attempt++) {
448
472
  try {
449
473
  const res = await (this.opts.fetch ?? fetch)(url, { ...init, signal })
450
- if (res.ok || !RETRYABLE.has(res.status) || attempt === retries) return res
474
+ if (res.ok) return res
475
+ const retryable = RETRYABLE.has(res.status)
476
+ const tier = classify({ status: res.status, attempt, retryable })
477
+ if (tier === "fail" || attempt === retries) return res // caller surfaces the non-ok status
451
478
  const ra = Number(res.headers.get("retry-after"))
452
479
  await delay(ra ? ra * 1000 : base * 2 ** attempt + Math.random() * 100, signal)
453
480
  } catch (e) {
454
481
  if (signal.aborted) throw e // abort/timeout: don't retry
455
482
  lastErr = e
456
- if (attempt === retries) throw e
483
+ const tier = classify({ error: e, attempt, retryable: true })
484
+ if (tier === "fail" || attempt === retries) throw e
457
485
  await delay(base * 2 ** attempt + Math.random() * 100, signal)
458
486
  }
459
487
  }
package/src/toolkit.ts CHANGED
@@ -21,6 +21,13 @@ export interface ToolkitOptions {
21
21
  skillProvider?: () => SkillDef[] | Promise<SkillDef[]>
22
22
  /** Per-agent skill allowlist keyed on name; same semantics as the MCP tools filter (§3, S2). */
23
23
  skillsFilter?: Record<string, boolean>
24
+ /** Drop tools by their FINAL exposed name across every source (MCP `server_tool`, builtins,
25
+ * native, a2a, extras), applied after aggregation — the simple "just turn these off" list.
26
+ * Empty/undefined ⇒ nothing disabled. Composes with builtins/skillsFilter/server allowlists. */
27
+ disableTools?: string[]
28
+ /** Drop skills by name from the `skill` catalog — sugar over a drop-list in `skillsFilter`
29
+ * (each name folded in as `false`, without overriding an explicit filter entry). */
30
+ disableSkills?: string[]
24
31
  /** Sibling-file sample cap: 0 ⇒ default 10, n>0 ⇒ cap, -1 ⇒ omit <skill_files> (§3, S5). */
25
32
  skillSampleLimit?: number
26
33
  extraTools?: Tool[]
@@ -52,14 +59,18 @@ export class Toolkit {
52
59
  private readonly a2aConfig: A2AConfig | undefined,
53
60
  /** MCP serve profile read from a top-level `mcpServer` config block. */
54
61
  private readonly mcpServerConfig: MCPServeConfig | undefined,
62
+ /** Final-name drop-list (§4A) applied across every source after aggregation. */
63
+ disableTools: string[] = [],
55
64
  ) {
56
65
  // Builtins are the lowest-precedence source: a host extraTools entry with
57
66
  // the same name shadows a builtin (SPEC §4). Drop shadowed builtins up
58
67
  // front, then apply the normal first-wins dedupe for MCP/skill/agents/extras.
59
68
  const extraNames = new Set(extraTools.map((t) => t.name))
60
69
  const activeBuiltins = builtins.filter((b) => !extraNames.has(b.name))
70
+ const disabled = new Set(disableTools)
61
71
  const all = [...(mcp?.tools ?? []), ...(skill ? [skill.tool] : []), ...activeBuiltins, ...agents, ...extraTools]
62
72
  for (const t of all) {
73
+ if (disabled.has(t.name)) continue // §4A drop-list by final exposed name
63
74
  if (this.byName.has(t.name)) {
64
75
  console.warn(`[toolnexus] duplicate tool name "${t.name}" — keeping first`)
65
76
  continue
@@ -85,10 +96,16 @@ export class Toolkit {
85
96
  }
86
97
  }
87
98
  const dataDefs = [...(opts.skills ?? []), ...providerDefs]
99
+ // disableSkills is sugar over a skillsFilter drop-list: fold each name in as false
100
+ // WITHOUT overriding an explicit skillsFilter entry (explicit entries win).
101
+ const filter =
102
+ opts.disableSkills && opts.disableSkills.length > 0
103
+ ? { ...Object.fromEntries(opts.disableSkills.map((n) => [n, false])), ...opts.skillsFilter }
104
+ : opts.skillsFilter
88
105
  skill = loadSkills({
89
106
  dirs: opts.skillsDir,
90
107
  skills: dataDefs.length > 0 ? dataDefs : undefined,
91
- filter: opts.skillsFilter,
108
+ filter,
92
109
  sampleLimit: opts.skillSampleLimit,
93
110
  })
94
111
  }
@@ -137,7 +154,7 @@ export class Toolkit {
137
154
  mcpServerConfig = (opts.mcpConfig as { mcpServer?: MCPServeConfig }).mcpServer
138
155
  }
139
156
 
140
- return new Toolkit(mcp, skill, builtins, agentToolsFlat, opts.extraTools ?? [], a2aConfig, mcpServerConfig)
157
+ return new Toolkit(mcp, skill, builtins, agentToolsFlat, opts.extraTools ?? [], a2aConfig, mcpServerConfig, opts.disableTools ?? [])
141
158
  }
142
159
 
143
160
  tools(): Tool[] {