toolnexus 0.9.4 → 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 +20 -1
- package/dist/client.js +12 -3
- package/dist/toolkit.d.ts +7 -0
- package/dist/toolkit.js +13 -3
- package/package.json +1 -1
- package/src/client.ts +31 -3
- package/src/toolkit.ts +19 -2
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
|
|
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
|
-
|
|
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
|
|
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
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
|
|
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
|
-
|
|
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
|
|
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[] {
|