@juspay/neurolink 9.90.0 → 9.91.1
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/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +370 -361
- package/dist/core/baseProvider.d.ts +37 -3
- package/dist/core/baseProvider.js +167 -44
- package/dist/core/modules/GenerationHandler.js +7 -1
- package/dist/core/modules/ToolsManager.js +5 -0
- package/dist/core/toolDedup.js +4 -1
- package/dist/lib/core/baseProvider.d.ts +37 -3
- package/dist/lib/core/baseProvider.js +167 -44
- package/dist/lib/core/modules/GenerationHandler.js +7 -1
- package/dist/lib/core/modules/ToolsManager.js +5 -0
- package/dist/lib/core/toolDedup.js +4 -1
- package/dist/lib/mcp/toolRegistry.js +7 -2
- package/dist/lib/neurolink.d.ts +31 -1
- package/dist/lib/neurolink.js +151 -26
- package/dist/lib/providers/sagemaker/client.d.ts +9 -0
- package/dist/lib/providers/sagemaker/client.js +75 -30
- package/dist/lib/server/routes/agentRoutes.js +25 -2
- package/dist/lib/tools/toolDiscovery.d.ts +48 -0
- package/dist/lib/tools/toolDiscovery.js +231 -0
- package/dist/lib/tools/toolGate.d.ts +16 -0
- package/dist/lib/tools/toolGate.js +51 -0
- package/dist/lib/tools/toolPolicy.d.ts +40 -0
- package/dist/lib/tools/toolPolicy.js +194 -0
- package/dist/lib/types/config.d.ts +43 -3
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/providers.d.ts +8 -0
- package/dist/lib/types/toolResolution.d.ts +73 -0
- package/dist/lib/types/toolResolution.js +11 -0
- package/dist/mcp/toolRegistry.js +7 -2
- package/dist/neurolink.d.ts +31 -1
- package/dist/neurolink.js +151 -26
- package/dist/providers/sagemaker/client.d.ts +9 -0
- package/dist/providers/sagemaker/client.js +75 -30
- package/dist/server/routes/agentRoutes.js +25 -2
- package/dist/tools/toolDiscovery.d.ts +48 -0
- package/dist/tools/toolDiscovery.js +230 -0
- package/dist/tools/toolGate.d.ts +16 -0
- package/dist/tools/toolGate.js +50 -0
- package/dist/tools/toolPolicy.d.ts +40 -0
- package/dist/tools/toolPolicy.js +193 -0
- package/dist/types/config.d.ts +43 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/providers.d.ts +8 -0
- package/dist/types/toolResolution.d.ts +73 -0
- package/dist/types/toolResolution.js +10 -0
- package/package.json +3 -2
|
@@ -88,7 +88,13 @@ export class GenerationHandler {
|
|
|
88
88
|
// Non-Anthropic providers harmlessly ignore unknown providerOptions.
|
|
89
89
|
// Note: The AI SDK Tool type doesn't yet include providerOptions, so we
|
|
90
90
|
// use a type assertion. The Anthropic adapter reads this at runtime.
|
|
91
|
-
|
|
91
|
+
//
|
|
92
|
+
// Deliberately NOT a clone: the record is call-scoped (built fresh in
|
|
93
|
+
// BaseProvider.prepareGenerationContext) and the AI SDK re-reads it on
|
|
94
|
+
// every agent-loop step, so `search_tools` hydration (tools.discovery)
|
|
95
|
+
// can add discovered tools mid-loop and have them callable on the next
|
|
96
|
+
// step. A clone would freeze the tool set for the whole call.
|
|
97
|
+
const toolsWithCache = tools;
|
|
92
98
|
if (isAnthropicProvider &&
|
|
93
99
|
shouldUseTools &&
|
|
94
100
|
Object.keys(toolsWithCache).length > 0) {
|
|
@@ -258,6 +258,11 @@ export class ToolsManager {
|
|
|
258
258
|
`... and ${toolNames.length - 10} more`,
|
|
259
259
|
],
|
|
260
260
|
});
|
|
261
|
+
// NOTE: insertion order here is phase order (direct → custom →
|
|
262
|
+
// external MCP), which the signature-dedup pass depends on (keep-first
|
|
263
|
+
// must prefer built-in implementations). Deterministic name-sorting
|
|
264
|
+
// for prompt-cache stability happens AFTER filtering+dedup, in
|
|
265
|
+
// BaseProvider.applyToolFiltering.
|
|
261
266
|
return tools;
|
|
262
267
|
});
|
|
263
268
|
}
|
|
@@ -171,7 +171,10 @@ export function dedupeTools(tools, options) {
|
|
|
171
171
|
if (representativeOf.size === 0) {
|
|
172
172
|
return noOp;
|
|
173
173
|
}
|
|
174
|
-
|
|
174
|
+
// Null prototype: a tool named "__proto__" must become an own entry,
|
|
175
|
+
// not a prototype mutation that silently drops it (mirrors the tool
|
|
176
|
+
// records built in BaseProvider.sortToolRecord / applyToolGate).
|
|
177
|
+
const dedupedTools = Object.create(null);
|
|
175
178
|
const removed = [];
|
|
176
179
|
for (const [name, tool] of entries) {
|
|
177
180
|
const rep = representativeOf.get(name);
|
|
@@ -7,6 +7,7 @@ import { registryLogger } from "../utils/logger.js";
|
|
|
7
7
|
import { randomUUID } from "crypto";
|
|
8
8
|
import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
|
|
9
9
|
import { directAgentTools } from "../agent/directTools.js";
|
|
10
|
+
import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
|
|
10
11
|
import { detectCategory, createMCPServerInfo } from "../utils/mcpDefaults.js";
|
|
11
12
|
import { FlexibleToolValidator } from "./flexibleToolValidator.js";
|
|
12
13
|
import { ErrorFactory } from "../utils/errorHandling.js";
|
|
@@ -57,10 +58,14 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
59
60
|
const toolId = `direct.${toolName}`;
|
|
61
|
+
// Register the tool's real parameter schema (converted from Zod) instead
|
|
62
|
+
// of a `{}` placeholder — token-budget accounting and tool listings read
|
|
63
|
+
// this ToolInfo, and an empty schema makes both under-count reality.
|
|
64
|
+
const inputSchema = convertZodToJsonSchema(toolDef.inputSchema);
|
|
60
65
|
const toolInfo = {
|
|
61
66
|
name: toolName,
|
|
62
67
|
description: toolDef.description || `Direct tool: ${toolName}`,
|
|
63
|
-
inputSchema
|
|
68
|
+
inputSchema,
|
|
64
69
|
serverId: "direct",
|
|
65
70
|
category: detectCategory({ isBuiltIn: true, serverId: "direct" }),
|
|
66
71
|
};
|
|
@@ -98,7 +103,7 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
98
103
|
}
|
|
99
104
|
},
|
|
100
105
|
description: toolDef.description,
|
|
101
|
-
inputSchema
|
|
106
|
+
inputSchema,
|
|
102
107
|
});
|
|
103
108
|
registryLogger.debug(`Registered direct tool: ${toolName} as ${toolId}`);
|
|
104
109
|
}
|
package/dist/lib/neurolink.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Uses real MCP infrastructure for tool discovery and execution.
|
|
7
7
|
*/
|
|
8
8
|
import type { AgentDefinition, AgentNetworkConfig, NetworkExecutionInput, NetworkExecutionOptions, NetworkExecutionResult, NetworkStreamChunk } from "./types/index.js";
|
|
9
|
-
import type { CompactionConfig, CompactionResult, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext, ToolRoutingServerDescriptor, ToolDedupConfig } from "./types/index.js";
|
|
9
|
+
import type { CompactionConfig, CompactionResult, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext, ToolRoutingServerDescriptor, ToolDedupConfig, ToolConfig } from "./types/index.js";
|
|
10
10
|
import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
|
|
11
11
|
import type { RedisConversationMemoryManager } from "./core/redisConversationMemoryManager.js";
|
|
12
12
|
import { ExternalServerManager } from "./mcp/externalServerManager.js";
|
|
@@ -107,6 +107,9 @@ export declare class NeuroLink {
|
|
|
107
107
|
private toolRoutingCacheInstance?;
|
|
108
108
|
private toolRoutingVectorCache?;
|
|
109
109
|
private toolDedupConfig?;
|
|
110
|
+
private toolsConfig?;
|
|
111
|
+
/** Session-scoped pins of tools discovered via search_tools (tools.discovery mode). */
|
|
112
|
+
private discoveryPins;
|
|
110
113
|
private enableOrchestration;
|
|
111
114
|
private authProvider?;
|
|
112
115
|
private pendingAuthConfig?;
|
|
@@ -812,6 +815,11 @@ export declare class NeuroLink {
|
|
|
812
815
|
/**
|
|
813
816
|
* Apply per-call tool filtering (whitelist/blacklist) to a ToolInfo array.
|
|
814
817
|
* Used to filter the tool list before building the system prompt.
|
|
818
|
+
*
|
|
819
|
+
* Resolves the SAME policy as the native gate (`BaseProvider`
|
|
820
|
+
* `applyToolFiltering`) — per-call options plus the instance-level `tools`
|
|
821
|
+
* config — so what the model is told about tools never diverges from the
|
|
822
|
+
* tools it can actually call.
|
|
815
823
|
*/
|
|
816
824
|
private applyToolInfoFiltering;
|
|
817
825
|
private createToolAwareSystemPrompt;
|
|
@@ -1194,6 +1202,28 @@ export declare class NeuroLink {
|
|
|
1194
1202
|
* parameter through the full call stack.
|
|
1195
1203
|
*/
|
|
1196
1204
|
getToolDedupConfig(): ToolDedupConfig | undefined;
|
|
1205
|
+
/**
|
|
1206
|
+
* Returns the instance-level `tools` config (master switch, include/exclude
|
|
1207
|
+
* lists, discovery mode), or `undefined` when not provided at construction.
|
|
1208
|
+
*
|
|
1209
|
+
* Called by `BaseProvider.applyToolFiltering` so the tool gate composes the
|
|
1210
|
+
* instance policy with per-call options on every generate/stream call.
|
|
1211
|
+
*/
|
|
1212
|
+
getToolsConfig(): ToolConfig | undefined;
|
|
1213
|
+
/**
|
|
1214
|
+
* Tools discovered via `search_tools` for a session (`tools.discovery`
|
|
1215
|
+
* mode). Pinned tools are sent in full on every subsequent call of that
|
|
1216
|
+
* session instead of being deferred — discovery cost is paid once.
|
|
1217
|
+
* Reading refreshes the session's recency (LRU), so active conversations
|
|
1218
|
+
* are never the ones evicted at the session cap.
|
|
1219
|
+
*/
|
|
1220
|
+
getDiscoveryPins(sessionKey: string): ReadonlySet<string>;
|
|
1221
|
+
/**
|
|
1222
|
+
* Pin discovered tools to a session (called by the `search_tools`
|
|
1223
|
+
* meta-tool on hydration). Append-only within a session; the map is
|
|
1224
|
+
* bounded by evicting the least-recently-used session past 1000 sessions.
|
|
1225
|
+
*/
|
|
1226
|
+
pinDiscoveredTools(sessionKey: string, toolNames: string[]): void;
|
|
1197
1227
|
/**
|
|
1198
1228
|
* Curator P1-1: synchronous credential health check for a single provider.
|
|
1199
1229
|
*
|
package/dist/lib/neurolink.js
CHANGED
|
@@ -83,7 +83,10 @@ import { resolveModel } from "./utils/modelAliasResolver.js";
|
|
|
83
83
|
// Import orchestration components
|
|
84
84
|
import { ModelRouter } from "./utils/modelRouter.js";
|
|
85
85
|
import { getBestProvider } from "./utils/providerUtils.js";
|
|
86
|
-
import { isZodSchema } from "./utils/schemaConversion.js";
|
|
86
|
+
import { isZodSchema, convertZodToJsonSchema, } from "./utils/schemaConversion.js";
|
|
87
|
+
import { resolveToolPolicy } from "./tools/toolPolicy.js";
|
|
88
|
+
import { applyToolGate } from "./tools/toolGate.js";
|
|
89
|
+
import { directAgentTools } from "./agent/directTools.js";
|
|
87
90
|
import { BinaryTaskClassifier } from "./utils/taskClassifier.js";
|
|
88
91
|
// Tool detection and execution imports
|
|
89
92
|
// Transformation utilities
|
|
@@ -284,6 +287,30 @@ export function markStreamProviderEmittedGenerationEnd(options) {
|
|
|
284
287
|
* survives minification AND doesn't rely on method-name stability.
|
|
285
288
|
*/
|
|
286
289
|
export const NEUROLINK_BRAND = Symbol.for("@juspay/neurolink/sdk-brand");
|
|
290
|
+
/**
|
|
291
|
+
* Providers whose native tool-calling support is model-dependent or absent —
|
|
292
|
+
* i.e. every provider that overrides `supportsTools()` and can return false
|
|
293
|
+
* (verified against src/lib/providers: ollama and openrouter are
|
|
294
|
+
* model-dependent; huggingface is deployment-dependent; the rest are
|
|
295
|
+
* image/embedding providers). Only these still receive the full tool listing
|
|
296
|
+
* in the system prompt on the generate path, where no provider instance
|
|
297
|
+
* exists yet to ask directly; every other provider gets tool definitions
|
|
298
|
+
* natively via its `tools` parameter, so repeating them in the prompt was
|
|
299
|
+
* pure token duplication. The stream path asks the provider instance
|
|
300
|
+
* (`provider.supportsTools()`) instead of this list. Keep in sync with
|
|
301
|
+
* `supportsTools()` overrides when adding providers.
|
|
302
|
+
*/
|
|
303
|
+
const PROMPT_ONLY_TOOL_PROVIDERS = new Set([
|
|
304
|
+
"ollama",
|
|
305
|
+
"huggingface",
|
|
306
|
+
"openrouter",
|
|
307
|
+
"ideogram",
|
|
308
|
+
"recraft",
|
|
309
|
+
"replicate",
|
|
310
|
+
"stability",
|
|
311
|
+
"jina",
|
|
312
|
+
"voyage",
|
|
313
|
+
]);
|
|
287
314
|
/**
|
|
288
315
|
* Type-guard for opaque values that should be a {@link NeuroLink} instance.
|
|
289
316
|
*
|
|
@@ -379,6 +406,9 @@ export class NeuroLink {
|
|
|
379
406
|
toolRoutingVectorCache;
|
|
380
407
|
// Opt-in tool-signature deduplication config.
|
|
381
408
|
toolDedupConfig;
|
|
409
|
+
toolsConfig;
|
|
410
|
+
/** Session-scoped pins of tools discovered via search_tools (tools.discovery mode). */
|
|
411
|
+
discoveryPins = new Map();
|
|
382
412
|
// Add orchestration property
|
|
383
413
|
enableOrchestration;
|
|
384
414
|
// Authentication provider for secure access control
|
|
@@ -807,6 +837,11 @@ export class NeuroLink {
|
|
|
807
837
|
if (config?.toolDedup) {
|
|
808
838
|
this.toolDedupConfig = { ...config.toolDedup };
|
|
809
839
|
}
|
|
840
|
+
if (config?.tools) {
|
|
841
|
+
// Shallow-clone so later caller mutations of the config object don't
|
|
842
|
+
// change this instance's tool policy mid-flight.
|
|
843
|
+
this.toolsConfig = { ...config.tools };
|
|
844
|
+
}
|
|
810
845
|
// ModelPool: build one instance from config; null when not configured.
|
|
811
846
|
this.modelPool = config?.modelPool ? new ModelPool(config.modelPool) : null;
|
|
812
847
|
// RequestRouter: store the host-supplied function; null when not configured.
|
|
@@ -1108,10 +1143,15 @@ export class NeuroLink {
|
|
|
1108
1143
|
// Use void to handle async registration without blocking constructor
|
|
1109
1144
|
const registrations = Object.entries(fileTools).map(async ([toolName, toolDef]) => {
|
|
1110
1145
|
const toolId = `direct.${toolName}`;
|
|
1146
|
+
// Register the real parameter schema (converted from Zod) instead of a
|
|
1147
|
+
// `{}` placeholder so tool listings and token-budget accounting see the
|
|
1148
|
+
// schema that actually ships to providers.
|
|
1149
|
+
const fileToolSchema = convertZodToJsonSchema((toolDef.inputSchema ??
|
|
1150
|
+
toolDef.parameters));
|
|
1111
1151
|
const toolInfo = {
|
|
1112
1152
|
name: toolName,
|
|
1113
1153
|
description: toolDef.description || `File tool: ${toolName}`,
|
|
1114
|
-
inputSchema:
|
|
1154
|
+
inputSchema: fileToolSchema,
|
|
1115
1155
|
serverId: "direct",
|
|
1116
1156
|
category: "built-in",
|
|
1117
1157
|
};
|
|
@@ -5143,10 +5183,21 @@ Current user's request: ${currentInput}`;
|
|
|
5143
5183
|
const circuitBreakerNote = unavailableTools.length > 0
|
|
5144
5184
|
? `\n\nNOTE: The following tools are temporarily unavailable due to repeated failures: ${unavailableTools.join(", ")}. Do not attempt to call these tools.`
|
|
5145
5185
|
: "";
|
|
5186
|
+
// Circuit-breaker-open tools are ENFORCED out of the native tool set via
|
|
5187
|
+
// the per-call denylist — asking the model nicely (the note above) is
|
|
5188
|
+
// informational, exclusion is the guarantee. Previously only the
|
|
5189
|
+
// system-prompt listing was filtered; the native array still shipped them.
|
|
5190
|
+
if (unavailableTools.length > 0) {
|
|
5191
|
+
options.excludeTools = [
|
|
5192
|
+
...new Set([...(options.excludeTools ?? []), ...unavailableTools]),
|
|
5193
|
+
];
|
|
5194
|
+
}
|
|
5195
|
+
// Providers with native tool calling receive full definitions via their
|
|
5196
|
+
// `tools` parameter; only prompt-based providers still get the listing.
|
|
5197
|
+
const nativeToolSupport = !PROMPT_ONLY_TOOL_PROVIDERS.has(String(providerName).toLowerCase());
|
|
5146
5198
|
const enhancedSystemPrompt = options.skipToolPromptInjection
|
|
5147
5199
|
? (options.systemPrompt || "") + circuitBreakerNote
|
|
5148
|
-
: this.createToolAwareSystemPrompt(options.systemPrompt, availableTools) +
|
|
5149
|
-
circuitBreakerNote;
|
|
5200
|
+
: this.createToolAwareSystemPrompt(options.systemPrompt, availableTools, nativeToolSupport) + circuitBreakerNote;
|
|
5150
5201
|
logger.debug("Tool-aware system prompt created", {
|
|
5151
5202
|
requestId,
|
|
5152
5203
|
originalPromptLength: options.systemPrompt?.length || 0,
|
|
@@ -5900,48 +5951,64 @@ Current user's request: ${currentInput}`;
|
|
|
5900
5951
|
/**
|
|
5901
5952
|
* Apply per-call tool filtering (whitelist/blacklist) to a ToolInfo array.
|
|
5902
5953
|
* Used to filter the tool list before building the system prompt.
|
|
5954
|
+
*
|
|
5955
|
+
* Resolves the SAME policy as the native gate (`BaseProvider`
|
|
5956
|
+
* `applyToolFiltering`) — per-call options plus the instance-level `tools`
|
|
5957
|
+
* config — so what the model is told about tools never diverges from the
|
|
5958
|
+
* tools it can actually call.
|
|
5903
5959
|
*/
|
|
5904
5960
|
applyToolInfoFiltering(tools, options) {
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
const denySet = new Set(options.excludeTools);
|
|
5921
|
-
filtered = filtered.filter((t) => !denySet.has(t.name));
|
|
5961
|
+
const policy = resolveToolPolicy({
|
|
5962
|
+
options: {
|
|
5963
|
+
disableTools: options.disableTools,
|
|
5964
|
+
toolFilter: options.toolFilter,
|
|
5965
|
+
enabledToolNames: options.enabledToolNames,
|
|
5966
|
+
excludeTools: options.excludeTools,
|
|
5967
|
+
},
|
|
5968
|
+
instanceConfig: this.toolsConfig,
|
|
5969
|
+
builtinToolNames: Object.keys(directAgentTools),
|
|
5970
|
+
});
|
|
5971
|
+
const record = {};
|
|
5972
|
+
for (const tool of tools) {
|
|
5973
|
+
if (!(tool.name in record)) {
|
|
5974
|
+
record[tool.name] = tool;
|
|
5975
|
+
}
|
|
5922
5976
|
}
|
|
5977
|
+
const gated = applyToolGate(record, policy);
|
|
5978
|
+
const filtered = tools.filter((t) => t.name in gated);
|
|
5923
5979
|
if (filtered.length !== tools.length) {
|
|
5924
5980
|
logger.debug(`Tool info filtering applied for system prompt`, {
|
|
5925
5981
|
beforeCount: tools.length,
|
|
5926
5982
|
afterCount: filtered.length,
|
|
5983
|
+
policySources: policy.sources,
|
|
5927
5984
|
toolFilter: options.toolFilter,
|
|
5928
5985
|
excludeTools: options.excludeTools,
|
|
5929
5986
|
});
|
|
5930
5987
|
}
|
|
5931
5988
|
return filtered;
|
|
5932
5989
|
}
|
|
5933
|
-
createToolAwareSystemPrompt(originalSystemPrompt, availableTools) {
|
|
5990
|
+
createToolAwareSystemPrompt(originalSystemPrompt, availableTools, nativeToolSupport) {
|
|
5934
5991
|
// AI prompt generation with tool analysis and structured logging
|
|
5935
5992
|
const promptGenerationData = {
|
|
5936
5993
|
originalPromptLength: originalSystemPrompt?.length || 0,
|
|
5937
5994
|
availableToolsCount: availableTools.length,
|
|
5938
5995
|
hasOriginalPrompt: !!originalSystemPrompt,
|
|
5996
|
+
nativeToolSupport,
|
|
5939
5997
|
};
|
|
5940
5998
|
logger.debug("AI prompt generation with tool schemas", promptGenerationData);
|
|
5941
5999
|
if (availableTools.length === 0) {
|
|
5942
6000
|
logger.debug("No tools available - returning original prompt");
|
|
5943
6001
|
return originalSystemPrompt || "";
|
|
5944
6002
|
}
|
|
6003
|
+
if (nativeToolSupport) {
|
|
6004
|
+
// The provider receives full tool definitions natively via its `tools`
|
|
6005
|
+
// parameter — repeating name/description/parameters here was pure
|
|
6006
|
+
// duplication (~850 tokens on the default surface, tens of thousands
|
|
6007
|
+
// with MCP servers attached). Keep only a short, static (cache-stable)
|
|
6008
|
+
// damping line so models don't over-reach for tools.
|
|
6009
|
+
return ((originalSystemPrompt || "") +
|
|
6010
|
+
"\n\nTools are available via native tool calling. Use them only when they genuinely improve your response; for creative or conversational requests, respond naturally without tools.");
|
|
6011
|
+
}
|
|
5945
6012
|
const toolDescriptions = transformToolsToDescriptions(availableTools.map((t) => ({
|
|
5946
6013
|
name: t.name,
|
|
5947
6014
|
description: t.description ?? "",
|
|
@@ -7746,10 +7813,13 @@ Current user's request: ${currentInput}`;
|
|
|
7746
7813
|
let availableTools = await this.getAllAvailableTools();
|
|
7747
7814
|
// Apply per-call tool filtering for system prompt tool descriptions
|
|
7748
7815
|
availableTools = this.applyToolInfoFiltering(availableTools, options);
|
|
7749
|
-
// Skip tool prompt injection if skipToolPromptInjection is true
|
|
7816
|
+
// Skip tool prompt injection if skipToolPromptInjection is true.
|
|
7817
|
+
// For providers with native tool calling (instance truth via
|
|
7818
|
+
// supportsTools()), the listing is skipped and only the short damping
|
|
7819
|
+
// line is appended — full definitions ship natively via `tools`.
|
|
7750
7820
|
const enhancedSystemPrompt = options.skipToolPromptInjection
|
|
7751
7821
|
? options.systemPrompt || ""
|
|
7752
|
-
: this.createToolAwareSystemPrompt(options.systemPrompt, availableTools);
|
|
7822
|
+
: this.createToolAwareSystemPrompt(options.systemPrompt, availableTools, provider.supportsTools?.() ?? true);
|
|
7753
7823
|
// Get conversation messages for context.
|
|
7754
7824
|
// If the caller already supplied conversationMessages (e.g. proxy routes
|
|
7755
7825
|
// forwarding a multi-turn Claude request), honour them — including an
|
|
@@ -8450,6 +8520,58 @@ Current user's request: ${currentInput}`;
|
|
|
8450
8520
|
getToolDedupConfig() {
|
|
8451
8521
|
return this.toolDedupConfig;
|
|
8452
8522
|
}
|
|
8523
|
+
/**
|
|
8524
|
+
* Returns the instance-level `tools` config (master switch, include/exclude
|
|
8525
|
+
* lists, discovery mode), or `undefined` when not provided at construction.
|
|
8526
|
+
*
|
|
8527
|
+
* Called by `BaseProvider.applyToolFiltering` so the tool gate composes the
|
|
8528
|
+
* instance policy with per-call options on every generate/stream call.
|
|
8529
|
+
*/
|
|
8530
|
+
getToolsConfig() {
|
|
8531
|
+
return this.toolsConfig;
|
|
8532
|
+
}
|
|
8533
|
+
/**
|
|
8534
|
+
* Tools discovered via `search_tools` for a session (`tools.discovery`
|
|
8535
|
+
* mode). Pinned tools are sent in full on every subsequent call of that
|
|
8536
|
+
* session instead of being deferred — discovery cost is paid once.
|
|
8537
|
+
* Reading refreshes the session's recency (LRU), so active conversations
|
|
8538
|
+
* are never the ones evicted at the session cap.
|
|
8539
|
+
*/
|
|
8540
|
+
getDiscoveryPins(sessionKey) {
|
|
8541
|
+
const pins = this.discoveryPins.get(sessionKey);
|
|
8542
|
+
if (!pins) {
|
|
8543
|
+
return new Set();
|
|
8544
|
+
}
|
|
8545
|
+
// Map iteration order is insertion order — re-inserting on access turns
|
|
8546
|
+
// the eviction below into LRU rather than FIFO.
|
|
8547
|
+
this.discoveryPins.delete(sessionKey);
|
|
8548
|
+
this.discoveryPins.set(sessionKey, pins);
|
|
8549
|
+
return pins;
|
|
8550
|
+
}
|
|
8551
|
+
/**
|
|
8552
|
+
* Pin discovered tools to a session (called by the `search_tools`
|
|
8553
|
+
* meta-tool on hydration). Append-only within a session; the map is
|
|
8554
|
+
* bounded by evicting the least-recently-used session past 1000 sessions.
|
|
8555
|
+
*/
|
|
8556
|
+
pinDiscoveredTools(sessionKey, toolNames) {
|
|
8557
|
+
let pins = this.discoveryPins.get(sessionKey);
|
|
8558
|
+
if (!pins) {
|
|
8559
|
+
pins = new Set();
|
|
8560
|
+
}
|
|
8561
|
+
else {
|
|
8562
|
+
this.discoveryPins.delete(sessionKey);
|
|
8563
|
+
}
|
|
8564
|
+
this.discoveryPins.set(sessionKey, pins);
|
|
8565
|
+
for (const name of toolNames) {
|
|
8566
|
+
pins.add(name);
|
|
8567
|
+
}
|
|
8568
|
+
if (this.discoveryPins.size > 1000) {
|
|
8569
|
+
const oldest = this.discoveryPins.keys().next().value;
|
|
8570
|
+
if (oldest !== undefined) {
|
|
8571
|
+
this.discoveryPins.delete(oldest);
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
8574
|
+
}
|
|
8453
8575
|
/**
|
|
8454
8576
|
* Curator P1-1: synchronous credential health check for a single provider.
|
|
8455
8577
|
*
|
|
@@ -9809,8 +9931,11 @@ Current user's request: ${currentInput}`;
|
|
|
9809
9931
|
}
|
|
9810
9932
|
}
|
|
9811
9933
|
}
|
|
9812
|
-
// Return canonical ToolInfo[]; defer presentation transforms to call sites
|
|
9813
|
-
|
|
9934
|
+
// Return canonical ToolInfo[]; defer presentation transforms to call sites.
|
|
9935
|
+
// Name-sorted: external MCP discovery completes in parallel, so Map
|
|
9936
|
+
// insertion order varies across restarts — downstream consumers (tool
|
|
9937
|
+
// listings, prompt construction, budget accounting) need a stable order.
|
|
9938
|
+
const tools = uniqueTools.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
9814
9939
|
// Update the cache
|
|
9815
9940
|
this.toolCache = {
|
|
9816
9941
|
tools,
|
|
@@ -10,9 +10,18 @@ import type { SageMakerConfig, InvokeEndpointParams, InvokeEndpointResponse, Con
|
|
|
10
10
|
*/
|
|
11
11
|
export declare class SageMakerRuntimeClient {
|
|
12
12
|
private client;
|
|
13
|
+
private sdkModule;
|
|
13
14
|
private config;
|
|
14
15
|
private isDisposed;
|
|
15
16
|
constructor(config: SageMakerConfig);
|
|
17
|
+
/**
|
|
18
|
+
* Lazily load (and cache) the `@aws-sdk/client-sagemaker-runtime` module.
|
|
19
|
+
*/
|
|
20
|
+
private getSdk;
|
|
21
|
+
/**
|
|
22
|
+
* Lazily construct (and cache) the underlying AWS SDK client.
|
|
23
|
+
*/
|
|
24
|
+
private getClient;
|
|
16
25
|
/**
|
|
17
26
|
* Invoke a SageMaker endpoint for synchronous inference
|
|
18
27
|
*
|
|
@@ -4,38 +4,41 @@
|
|
|
4
4
|
* This module provides a wrapper around the AWS SDK SageMaker Runtime client
|
|
5
5
|
* with enhanced error handling, retry logic, and NeuroLink-specific features.
|
|
6
6
|
*/
|
|
7
|
-
import { SageMakerRuntimeClient as AWSClient, InvokeEndpointCommand, InvokeEndpointWithResponseStreamCommand, } from "@aws-sdk/client-sagemaker-runtime";
|
|
8
7
|
import { handleSageMakerError, SageMakerError, isRetryableError, getRetryDelay, } from "./errors.js";
|
|
9
8
|
import { logger } from "../../utils/logger.js";
|
|
9
|
+
/**
|
|
10
|
+
* Lazily load `@aws-sdk/client-sagemaker-runtime`.
|
|
11
|
+
*
|
|
12
|
+
* The package is an optional dependency: importing it only happens here, on
|
|
13
|
+
* first actual endpoint invocation, so constructing a SageMakerRuntimeClient
|
|
14
|
+
* (e.g. while the CLI builds its command tree at startup) never requires the
|
|
15
|
+
* SDK to be installed. If it's missing, surface a comprehensible, actionable
|
|
16
|
+
* error instead of a raw resolution failure.
|
|
17
|
+
*/
|
|
18
|
+
async function loadSageMakerRuntime() {
|
|
19
|
+
try {
|
|
20
|
+
return await import(/* @vite-ignore */ "@aws-sdk/client-sagemaker-runtime");
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
const e = err instanceof Error ? err : null;
|
|
24
|
+
if (e?.code === "ERR_MODULE_NOT_FOUND" &&
|
|
25
|
+
e.message.includes("client-sagemaker-runtime")) {
|
|
26
|
+
throw new Error('SageMaker inference requires "@aws-sdk/client-sagemaker-runtime". Install it with:\n pnpm add @aws-sdk/client-sagemaker-runtime', { cause: err });
|
|
27
|
+
}
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
10
31
|
/**
|
|
11
32
|
* Enhanced SageMaker Runtime client with retry logic and error handling
|
|
12
33
|
*/
|
|
13
34
|
export class SageMakerRuntimeClient {
|
|
14
|
-
client;
|
|
35
|
+
client = null;
|
|
36
|
+
sdkModule = null;
|
|
15
37
|
config;
|
|
16
38
|
isDisposed = false;
|
|
17
39
|
constructor(config) {
|
|
18
40
|
this.config = config;
|
|
19
|
-
|
|
20
|
-
this.client = new AWSClient({
|
|
21
|
-
region: config.region,
|
|
22
|
-
credentials: {
|
|
23
|
-
accessKeyId: config.accessKeyId,
|
|
24
|
-
secretAccessKey: config.secretAccessKey,
|
|
25
|
-
sessionToken: config.sessionToken,
|
|
26
|
-
},
|
|
27
|
-
maxAttempts: config.maxRetries || 3,
|
|
28
|
-
requestHandler: {
|
|
29
|
-
requestTimeout: config.timeout || 30000,
|
|
30
|
-
httpsAgent: {
|
|
31
|
-
// Keep connections alive for better performance
|
|
32
|
-
keepAlive: true,
|
|
33
|
-
maxSockets: 50,
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
...(config.endpoint && { endpoint: config.endpoint }),
|
|
37
|
-
});
|
|
38
|
-
logger.debug("SageMaker Runtime client initialized", {
|
|
41
|
+
logger.debug("SageMaker Runtime client configured", {
|
|
39
42
|
region: config.region,
|
|
40
43
|
timeout: config.timeout,
|
|
41
44
|
maxRetries: config.maxRetries,
|
|
@@ -43,6 +46,51 @@ export class SageMakerRuntimeClient {
|
|
|
43
46
|
customEndpoint: !!config.endpoint,
|
|
44
47
|
});
|
|
45
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Lazily load (and cache) the `@aws-sdk/client-sagemaker-runtime` module.
|
|
51
|
+
*/
|
|
52
|
+
async getSdk() {
|
|
53
|
+
if (!this.sdkModule) {
|
|
54
|
+
this.sdkModule = await loadSageMakerRuntime();
|
|
55
|
+
}
|
|
56
|
+
return this.sdkModule;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Lazily construct (and cache) the underlying AWS SDK client.
|
|
60
|
+
*/
|
|
61
|
+
async getClient() {
|
|
62
|
+
if (this.isDisposed) {
|
|
63
|
+
throw new SageMakerError("Cannot perform operation on disposed SageMaker client", {
|
|
64
|
+
code: "VALIDATION_ERROR",
|
|
65
|
+
statusCode: 400,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (!this.client) {
|
|
69
|
+
const { SageMakerRuntimeClient: AWSClientCtor } = await this.getSdk();
|
|
70
|
+
this.client = new AWSClientCtor({
|
|
71
|
+
region: this.config.region,
|
|
72
|
+
credentials: {
|
|
73
|
+
accessKeyId: this.config.accessKeyId,
|
|
74
|
+
secretAccessKey: this.config.secretAccessKey,
|
|
75
|
+
sessionToken: this.config.sessionToken,
|
|
76
|
+
},
|
|
77
|
+
maxAttempts: this.config.maxRetries || 3,
|
|
78
|
+
requestHandler: {
|
|
79
|
+
requestTimeout: this.config.timeout || 30000,
|
|
80
|
+
httpsAgent: {
|
|
81
|
+
// Keep connections alive for better performance
|
|
82
|
+
keepAlive: true,
|
|
83
|
+
maxSockets: 50,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
...(this.config.endpoint && { endpoint: this.config.endpoint }),
|
|
87
|
+
});
|
|
88
|
+
logger.debug("SageMaker Runtime AWS SDK client initialized", {
|
|
89
|
+
region: this.config.region,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return this.client;
|
|
93
|
+
}
|
|
46
94
|
/**
|
|
47
95
|
* Invoke a SageMaker endpoint for synchronous inference
|
|
48
96
|
*
|
|
@@ -61,6 +109,7 @@ export class SageMakerRuntimeClient {
|
|
|
61
109
|
? params.Body.length
|
|
62
110
|
: params.Body?.length || 0,
|
|
63
111
|
});
|
|
112
|
+
const { InvokeEndpointCommand } = await this.getSdk();
|
|
64
113
|
// Prepare the command input
|
|
65
114
|
const input = {
|
|
66
115
|
EndpointName: params.EndpointName,
|
|
@@ -73,10 +122,7 @@ export class SageMakerRuntimeClient {
|
|
|
73
122
|
InferenceId: params.InferenceId,
|
|
74
123
|
};
|
|
75
124
|
const command = new InvokeEndpointCommand(input);
|
|
76
|
-
const client = this.
|
|
77
|
-
if (!client) {
|
|
78
|
-
throw new Error("SageMaker client has been disposed");
|
|
79
|
-
}
|
|
125
|
+
const client = await this.getClient();
|
|
80
126
|
const response = (await this.executeWithRetry(() => client.send(command), params.EndpointName));
|
|
81
127
|
const duration = Date.now() - startTime;
|
|
82
128
|
logger.debug("SageMaker endpoint invocation successful", {
|
|
@@ -120,6 +166,7 @@ export class SageMakerRuntimeClient {
|
|
|
120
166
|
? params.Body.length
|
|
121
167
|
: params.Body?.length || 0,
|
|
122
168
|
});
|
|
169
|
+
const { InvokeEndpointWithResponseStreamCommand } = await this.getSdk();
|
|
123
170
|
// Prepare the command input for streaming
|
|
124
171
|
const input = {
|
|
125
172
|
EndpointName: params.EndpointName,
|
|
@@ -130,10 +177,7 @@ export class SageMakerRuntimeClient {
|
|
|
130
177
|
// Note: TargetModel, TargetVariant, InferenceId not available in streaming interface
|
|
131
178
|
};
|
|
132
179
|
const command = new InvokeEndpointWithResponseStreamCommand(input);
|
|
133
|
-
const client = this.
|
|
134
|
-
if (!client) {
|
|
135
|
-
throw new Error("SageMaker client has been disposed");
|
|
136
|
-
}
|
|
180
|
+
const client = await this.getClient();
|
|
137
181
|
const response = (await this.executeWithRetry(() => client.send(command), params.EndpointName));
|
|
138
182
|
logger.debug("SageMaker streaming invocation started", {
|
|
139
183
|
endpointName: params.EndpointName,
|
|
@@ -415,6 +459,7 @@ export class SageMakerRuntimeClient {
|
|
|
415
459
|
// Clear our client reference to enable garbage collection
|
|
416
460
|
// Note: AWS SDK v3 handles all internal resource cleanup automatically
|
|
417
461
|
this.client = null;
|
|
462
|
+
this.sdkModule = null;
|
|
418
463
|
logger.debug("SageMaker Runtime client disposed", {
|
|
419
464
|
note: "AWS SDK v3 handles internal resource cleanup automatically",
|
|
420
465
|
});
|
|
@@ -6,6 +6,27 @@ import { SpanStatusCode } from "@opentelemetry/api";
|
|
|
6
6
|
import { ProviderFactory } from "../../factories/providerFactory.js";
|
|
7
7
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
8
8
|
import { tracers } from "../../telemetry/tracers.js";
|
|
9
|
+
import { logger } from "../../utils/logger.js";
|
|
10
|
+
/**
|
|
11
|
+
* Resolve `request.tools` (an array of tool NAMES shared by the execute and
|
|
12
|
+
* stream endpoints) into a `toolFilter` whitelist. The field was previously
|
|
13
|
+
* accepted and silently ignored, so entries are validated against registered
|
|
14
|
+
* names and the whole field fails open (undefined — keep all tools) when
|
|
15
|
+
* nothing matches: existing clients sending unknown names must not lose
|
|
16
|
+
* their tool set.
|
|
17
|
+
*/
|
|
18
|
+
async function resolveRequestToolFilter(ctx, requestedTools) {
|
|
19
|
+
if (!Array.isArray(requestedTools) || requestedTools.length === 0) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const registered = new Set((await ctx.neurolink.getAllAvailableTools()).map((t) => t.name));
|
|
23
|
+
const known = requestedTools.filter((name) => typeof name === "string" && registered.has(name));
|
|
24
|
+
if (known.length > 0) {
|
|
25
|
+
return known;
|
|
26
|
+
}
|
|
27
|
+
logger.warn("[agentRoutes] Ignoring request.tools — no entries match registered tool names (legacy fail-open)", { requested: requestedTools });
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
9
30
|
import { createStreamRedactor } from "../utils/redaction.js";
|
|
10
31
|
import { AgentExecuteRequestSchema, createErrorResponse as createError, EmbedManyRequestSchema, EmbedRequestSchema, SkillCreateRequestSchema, SkillUpdateRequestSchema, validateRequest, } from "../utils/validation.js";
|
|
11
32
|
/**
|
|
@@ -65,6 +86,7 @@ export function createAgentRoutes(basePath = "/api") {
|
|
|
65
86
|
const input = typeof request.input === "string"
|
|
66
87
|
? { text: request.input }
|
|
67
88
|
: request.input;
|
|
89
|
+
const requestToolFilter = await resolveRequestToolFilter(ctx, request.tools);
|
|
68
90
|
const result = await ctx.neurolink.generate({
|
|
69
91
|
input,
|
|
70
92
|
provider: request.provider,
|
|
@@ -72,8 +94,7 @@ export function createAgentRoutes(basePath = "/api") {
|
|
|
72
94
|
systemPrompt: request.systemPrompt,
|
|
73
95
|
temperature: request.temperature,
|
|
74
96
|
maxTokens: request.maxTokens,
|
|
75
|
-
|
|
76
|
-
// If request.tools is an array of tool names, we skip them
|
|
97
|
+
...(requestToolFilter ? { toolFilter: requestToolFilter } : {}),
|
|
77
98
|
context: {
|
|
78
99
|
// When an authenticated user context exists (set by auth middleware),
|
|
79
100
|
// always use its IDs to prevent caller-supplied impersonation.
|
|
@@ -118,6 +139,7 @@ export function createAgentRoutes(basePath = "/api") {
|
|
|
118
139
|
const input = typeof request.input === "string"
|
|
119
140
|
? { text: request.input }
|
|
120
141
|
: request.input;
|
|
142
|
+
const streamToolFilter = await resolveRequestToolFilter(ctx, request.tools);
|
|
121
143
|
const result = await ctx.neurolink.stream({
|
|
122
144
|
input,
|
|
123
145
|
provider: request.provider,
|
|
@@ -125,6 +147,7 @@ export function createAgentRoutes(basePath = "/api") {
|
|
|
125
147
|
systemPrompt: request.systemPrompt,
|
|
126
148
|
temperature: request.temperature,
|
|
127
149
|
maxTokens: request.maxTokens,
|
|
150
|
+
...(streamToolFilter ? { toolFilter: streamToolFilter } : {}),
|
|
128
151
|
context: {
|
|
129
152
|
// When an authenticated user context exists (set by auth middleware),
|
|
130
153
|
// always use its IDs to prevent caller-supplied impersonation.
|