@juspay/neurolink 12.47.13 → 12.48.0
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 +3 -3
- package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
- package/dist/browser/neurolink.min.js +578 -565
- package/dist/cli/factories/commandFactory.d.ts +7 -0
- package/dist/cli/factories/commandFactory.js +26 -0
- package/dist/cli/loop/optionsSchema.js +9 -0
- package/dist/context/budgetChecker.js +54 -2
- package/dist/context/contextDecision.js +2 -0
- package/dist/context/toolPairRepair.d.ts +12 -0
- package/dist/context/toolPairRepair.js +29 -3
- package/dist/core/baseProvider.d.ts +92 -3
- package/dist/core/baseProvider.js +254 -10
- package/dist/core/conversationMemoryManager.js +17 -2
- package/dist/core/loopEngine.js +1 -1
- package/dist/core/modules/MessageBuilder.js +11 -3
- package/dist/core/modules/TelemetryHandler.d.ts +2 -2
- package/dist/core/modules/ToolsManager.d.ts +35 -0
- package/dist/core/modules/ToolsManager.js +159 -55
- package/dist/core/nativeGenerateLoop.d.ts +58 -1
- package/dist/core/nativeGenerateLoop.js +277 -3
- package/dist/core/redisConversationMemoryManager.js +10 -3
- package/dist/core/resolveRequestKind.d.ts +24 -4
- package/dist/core/resolveRequestKind.js +31 -4
- package/dist/core/toolExecutionRecorder.d.ts +6 -0
- package/dist/core/toolExecutionRecorder.js +24 -2
- package/dist/core/toolRoutingDecision.js +1 -0
- package/dist/factories/mediaHandlerCatalog.js +5 -0
- package/dist/factories/providerDescriptors.js +15 -0
- package/dist/files/fileTools.d.ts +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +9 -2
- package/dist/memory/condensationPrompt.d.ts +8 -0
- package/dist/memory/condensationPrompt.js +29 -0
- package/dist/memory/hippocampusInitializer.d.ts +14 -2
- package/dist/memory/hippocampusInitializer.js +64 -3
- package/dist/neurolink.d.ts +252 -3
- package/dist/neurolink.js +898 -43
- package/dist/providers/amazonBedrock/client.js +6 -0
- package/dist/providers/amazonSagemaker.js +14 -1
- package/dist/providers/anthropic/client.js +148 -19
- package/dist/providers/anthropic/loopAdapter.js +2 -2
- package/dist/providers/googleAiStudio/client.js +29 -14
- package/dist/providers/openaiChatCompletionsBase.js +103 -23
- package/dist/providers/systemOneDecision.d.ts +6 -0
- package/dist/providers/systemOneDecision.js +14 -63
- package/dist/providers/typesafe.d.ts +6 -0
- package/dist/providers/typesafe.js +6 -0
- package/dist/rag/retrieval/searchDecision.js +1 -0
- package/dist/routing/classifierRouter.js +6 -5
- package/dist/routing/classifierStrategies.js +8 -1
- package/dist/types/classifierRouter.d.ts +16 -3
- package/dist/types/common.d.ts +2 -0
- package/dist/types/config.d.ts +9 -0
- package/dist/types/context.d.ts +8 -1
- package/dist/types/conversation.d.ts +87 -0
- package/dist/types/decision.d.ts +210 -1
- package/dist/types/decision.js +9 -1
- package/dist/types/dispatch.d.ts +11 -4
- package/dist/types/generate.d.ts +118 -54
- package/dist/types/loopEngine.d.ts +7 -2
- package/dist/types/memory.d.ts +177 -9
- package/dist/types/openaiCompatible.d.ts +11 -3
- package/dist/types/stream.d.ts +13 -16
- package/dist/types/stt.d.ts +58 -1
- package/dist/types/tools.d.ts +14 -0
- package/dist/types/tools.js +8 -0
- package/dist/types/tts.d.ts +108 -0
- package/dist/types/voice.d.ts +41 -4
- package/dist/utils/decisionAnswers.d.ts +31 -1
- package/dist/utils/decisionAnswers.js +114 -0
- package/dist/utils/decisionLimits.d.ts +39 -0
- package/dist/utils/decisionLimits.js +116 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.js +9 -0
- package/dist/utils/messageBuilder.d.ts +45 -1
- package/dist/utils/messageBuilder.js +393 -16
- package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
- package/dist/utils/multimodalOptionsBuilder.js +1 -0
- package/dist/utils/speechText.d.ts +41 -0
- package/dist/utils/speechText.js +364 -0
- package/dist/utils/toolResultStatus.d.ts +26 -0
- package/dist/utils/toolResultStatus.js +73 -0
- package/dist/utils/ttsProcessor.d.ts +29 -0
- package/dist/utils/ttsProcessor.js +84 -2
- package/dist/voice/index.d.ts +1 -0
- package/dist/voice/index.js +3 -0
- package/dist/voice/providers/ElevenLabsSTT.d.ts +42 -0
- package/dist/voice/providers/ElevenLabsSTT.js +351 -0
- package/dist/voice/providers/ElevenLabsTTS.d.ts +81 -1
- package/dist/voice/providers/ElevenLabsTTS.js +415 -74
- package/docs-site/static/search-index.json +48 -33
- package/package.json +5 -2
- package/dist/utils/toolEndEmitter.d.ts +0 -25
- package/dist/utils/toolEndEmitter.js +0 -64
|
@@ -33,9 +33,8 @@ import { redactUrlCredentials } from "../utils/logSanitize.js";
|
|
|
33
33
|
import { NoOutputGeneratedError } from "../utils/generationErrors.js";
|
|
34
34
|
import { buildNoOutputSentinel, stampNoOutputSpan, } from "../utils/noOutputSentinel.js";
|
|
35
35
|
import { composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../utils/timeout.js";
|
|
36
|
-
import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
|
|
37
36
|
import { resolveRequestKind } from "../core/resolveRequestKind.js";
|
|
38
|
-
import { appendJsonSchemaInstruction, hasNativeDoGenerate, runNativeGenerateLoop, } from "../core/nativeGenerateLoop.js";
|
|
37
|
+
import { appendJsonSchemaInstruction, hasNativeDoGenerate, resolveStepToolChoice, runNativeGenerateLoop, toPrepareStepRecord, } from "../core/nativeGenerateLoop.js";
|
|
39
38
|
import { resolveToolExecutionRecords, toolCallsFromSummaries, } from "../core/toolExecutionRecorder.js";
|
|
40
39
|
import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
|
|
41
40
|
import { coerceJsonToSchema, recoverScalarRoot, schemaAccepts, } from "../utils/json/coerce.js";
|
|
@@ -908,6 +907,11 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
908
907
|
}
|
|
909
908
|
const doGenerate = model.doGenerate.bind(model);
|
|
910
909
|
const shouldUseTools = !options.disableTools && this.supportsTools();
|
|
910
|
+
// `options.tools` arrives from BaseProvider's merge (getToolsForStream)
|
|
911
|
+
// already event-wrapped, recorder-wrapped and discovery-partitioned. It
|
|
912
|
+
// must be used AS IS: copying it would drop the discovery record's null
|
|
913
|
+
// prototype and non-enumerable resolver, and tools search_tools hydrates
|
|
914
|
+
// later land only in the original object.
|
|
911
915
|
const toolsRecord = shouldUseTools
|
|
912
916
|
? options.tools || (await this.getAllTools())
|
|
913
917
|
: {};
|
|
@@ -979,6 +983,11 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
979
983
|
...(withTools && hasTools && options.toolChoice
|
|
980
984
|
? { toolChoice: resolveToolChoice(options, toolsRecord, true) }
|
|
981
985
|
: {}),
|
|
986
|
+
...(options.toolChoiceSteps !== undefined
|
|
987
|
+
? { toolChoiceSteps: options.toolChoiceSteps }
|
|
988
|
+
: {}),
|
|
989
|
+
...(options.prepareStep ? { prepareStep: options.prepareStep } : {}),
|
|
990
|
+
modelId,
|
|
982
991
|
// The per-call `timeout` keeps its per-MODEL-CALL meaning once
|
|
983
992
|
// `turnTimeoutMs` owns the whole-turn deadline, and it reaches the
|
|
984
993
|
// model layer only through this channel. Without it each step fell
|
|
@@ -1008,6 +1017,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1008
1017
|
runStep: (call) => withProviderRetry(call, trace.getActiveSpan() ?? undefined, `${this.providerName} generate`, undefined, options.abortSignal).catch((err) => {
|
|
1009
1018
|
throw this.handleProviderError(err);
|
|
1010
1019
|
}),
|
|
1020
|
+
onRejectedToolCall: (name, error, id) => this.emitRejectedToolCall(name, error, id),
|
|
1011
1021
|
}, toolExecutionSummaries);
|
|
1012
1022
|
};
|
|
1013
1023
|
// Structured output rides `response_format` first. When a vendor rejects
|
|
@@ -1138,7 +1148,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1138
1148
|
toolExecutions: resolveToolExecutionRecords(options, transformToolExecutions(toolExecutionSummaries)),
|
|
1139
1149
|
enhancedWithTools: toolsUsed.length > 0,
|
|
1140
1150
|
};
|
|
1141
|
-
return this.finalizeNativeGenerate(enhanced, options, startTime);
|
|
1151
|
+
return this.finalizeNativeGenerate(enhanced, options, startTime, toolExecutionSummaries);
|
|
1142
1152
|
}
|
|
1143
1153
|
/**
|
|
1144
1154
|
* Recover structured output on a turn where tools forced `response_format`
|
|
@@ -1454,6 +1464,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1454
1464
|
doGenerate,
|
|
1455
1465
|
conversation: conv,
|
|
1456
1466
|
toolsRecord: {},
|
|
1467
|
+
modelId,
|
|
1457
1468
|
...(format ? { responseFormat: format } : {}),
|
|
1458
1469
|
maxSteps: 1,
|
|
1459
1470
|
...(options.maxTokens
|
|
@@ -1575,7 +1586,10 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1575
1586
|
let toolsRecord;
|
|
1576
1587
|
let wireNameMaps;
|
|
1577
1588
|
let openAITools;
|
|
1578
|
-
|
|
1589
|
+
// The turn's tool choice in NeuroLink shape; translated to the wire per
|
|
1590
|
+
// step (see stepToolChoice below) so a forced choice can lapse after
|
|
1591
|
+
// `toolChoiceSteps` instead of compelling a tool call on every step.
|
|
1592
|
+
let turnToolChoice;
|
|
1579
1593
|
// The prompt is kept in its pre-wire shape. Model middleware transforms
|
|
1580
1594
|
// `params.prompt`, and the conversion to the chat-completions wire format
|
|
1581
1595
|
// has to happen AFTER that or the transform would be discarded.
|
|
@@ -1583,6 +1597,8 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1583
1597
|
try {
|
|
1584
1598
|
modelId = await this.resolveModelName();
|
|
1585
1599
|
const shouldUseTools = !options.disableTools && this.supportsTools();
|
|
1600
|
+
// Same identity rule as executeNativeGenerate: the merged record is
|
|
1601
|
+
// the discovery hot record and is never copied here.
|
|
1586
1602
|
toolsRecord = shouldUseTools
|
|
1587
1603
|
? options.tools || (await this.getAllTools())
|
|
1588
1604
|
: {};
|
|
@@ -1594,7 +1610,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1594
1610
|
openAITools = shouldUseTools
|
|
1595
1611
|
? buildToolsForOpenAI(toolsRecord, wireNameMaps?.toWire)
|
|
1596
1612
|
: undefined;
|
|
1597
|
-
|
|
1613
|
+
turnToolChoice = resolveToolChoice(options, toolsRecord, shouldUseTools);
|
|
1598
1614
|
promptMessages = (await this.buildMessagesForStream(options));
|
|
1599
1615
|
}
|
|
1600
1616
|
catch (setupErr) {
|
|
@@ -1621,7 +1637,6 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1621
1637
|
const url = this.getChatCompletionsURL(modelId);
|
|
1622
1638
|
const fetchImpl = createProxyFetch();
|
|
1623
1639
|
const maxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
|
|
1624
|
-
const emitter = this.neurolink?.getEventEmitter();
|
|
1625
1640
|
const toolsUsed = [];
|
|
1626
1641
|
const toolExecutionSummaries = [];
|
|
1627
1642
|
const { usagePromise, finishPromise, resolveUsage, resolveFinish } = createDeferredAnalytics();
|
|
@@ -1715,10 +1730,22 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1715
1730
|
options: sampled,
|
|
1716
1731
|
conversation,
|
|
1717
1732
|
openAITools,
|
|
1718
|
-
|
|
1733
|
+
// No declared tools → no tool_choice, and the hook is not consulted:
|
|
1734
|
+
// a forced choice on a tool-less request is a 400.
|
|
1735
|
+
stepToolChoice: async (step, steps) => openAITools && openAITools.length > 0
|
|
1736
|
+
? mapNeuroLinkToolChoice(await resolveStepToolChoice({
|
|
1737
|
+
base: turnToolChoice,
|
|
1738
|
+
step,
|
|
1739
|
+
toolChoiceSteps: options.toolChoiceSteps,
|
|
1740
|
+
prepareStep: options.prepareStep,
|
|
1741
|
+
steps,
|
|
1742
|
+
maxSteps,
|
|
1743
|
+
model: modelId,
|
|
1744
|
+
...(abortSignal ? { abortSignal } : {}),
|
|
1745
|
+
}), wireNameMaps?.toWire)
|
|
1746
|
+
: undefined,
|
|
1719
1747
|
toolsRecord,
|
|
1720
1748
|
toolNameFromWire: wireNameMaps?.fromWire,
|
|
1721
|
-
emitter,
|
|
1722
1749
|
toolsUsed,
|
|
1723
1750
|
toolExecutionSummaries,
|
|
1724
1751
|
pushChunk: channel.push,
|
|
@@ -2012,7 +2039,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
2012
2039
|
return result;
|
|
2013
2040
|
}
|
|
2014
2041
|
async runStreamLoop(args) {
|
|
2015
|
-
const { maxSteps, modelId, url, fetchImpl, abortSignal, options, conversation, openAITools,
|
|
2042
|
+
const { maxSteps, modelId, url, fetchImpl, abortSignal, options, conversation, openAITools, stepToolChoice, toolsRecord, toolNameFromWire, toolsUsed, toolExecutionSummaries, pushChunk, closeChannel, resolveUsage, resolveFinish, responseFormat, onModelObserved, } = args;
|
|
2016
2043
|
// Hoisted above the try so the catch can resolve the usage accumulated
|
|
2017
2044
|
// by steps that completed BEFORE the failure — those steps were billed.
|
|
2018
2045
|
let stepFinish = null;
|
|
@@ -2049,7 +2076,10 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
2049
2076
|
// two different payloads would be meaningless.
|
|
2050
2077
|
let lastObservedPromptTokens;
|
|
2051
2078
|
let lastSentEstimate;
|
|
2079
|
+
// Completed-step records handed to a caller's prepareStep hook.
|
|
2080
|
+
const stepRecords = [];
|
|
2052
2081
|
for (let step = 0; step < maxSteps; step++) {
|
|
2082
|
+
const openAIToolChoice = await stepToolChoice(step, stepRecords);
|
|
2053
2083
|
// Mid-turn discovery sync: search_tools (tools.discovery) hydrates
|
|
2054
2084
|
// new tools into toolsRecord between steps. Dispatch already re-reads
|
|
2055
2085
|
// the record — this block makes the request DECLARE them so the model
|
|
@@ -2137,11 +2167,54 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
2137
2167
|
conversation,
|
|
2138
2168
|
toolsRecord,
|
|
2139
2169
|
toolNameFromWire: effectiveToolNameFromWire,
|
|
2140
|
-
emitter,
|
|
2141
2170
|
toolsUsed,
|
|
2142
2171
|
toolExecutionSummaries,
|
|
2143
2172
|
options,
|
|
2173
|
+
stepIndex: step,
|
|
2144
2174
|
});
|
|
2175
|
+
const executed = toolExecutionSummaries.slice(-stepResult.toolCalls.size);
|
|
2176
|
+
stepRecords.push(toPrepareStepRecord({
|
|
2177
|
+
stepNumber: step,
|
|
2178
|
+
content: [
|
|
2179
|
+
...(stepResult.text
|
|
2180
|
+
? [{ type: "text", text: stepResult.text }]
|
|
2181
|
+
: []),
|
|
2182
|
+
...executed.map((s) => ({
|
|
2183
|
+
type: "tool-call",
|
|
2184
|
+
toolCallId: s.toolCallId,
|
|
2185
|
+
toolName: s.toolName,
|
|
2186
|
+
input: s.input,
|
|
2187
|
+
})),
|
|
2188
|
+
],
|
|
2189
|
+
text: stepResult.text,
|
|
2190
|
+
toolCalls: executed.map((s) => ({
|
|
2191
|
+
toolName: s.toolName,
|
|
2192
|
+
toolCallId: s.toolCallId,
|
|
2193
|
+
input: s.input,
|
|
2194
|
+
})),
|
|
2195
|
+
toolResults: executed.map((s) => ({
|
|
2196
|
+
toolName: s.toolName,
|
|
2197
|
+
toolCallId: s.toolCallId,
|
|
2198
|
+
output: s.output,
|
|
2199
|
+
})),
|
|
2200
|
+
finishReason: "tool-calls",
|
|
2201
|
+
// prompt_tokens already includes cached_tokens; subtract it so
|
|
2202
|
+
// this agrees with the generate loop's toDeferredUsage, which
|
|
2203
|
+
// reports the disjoint noCache figure for the same field —
|
|
2204
|
+
// otherwise a cached step reports cached tokens twice (once
|
|
2205
|
+
// here, once in cacheReadTokens) and a prepareStep hook that
|
|
2206
|
+
// budgets on steps[].usage sees different numbers from
|
|
2207
|
+
// stream() and generate() for the same step.
|
|
2208
|
+
inputTokens: Math.max(0, (stepResult.usage?.prompt_tokens ?? 0) -
|
|
2209
|
+
(stepResult.usage?.prompt_tokens_details?.cached_tokens ?? 0)),
|
|
2210
|
+
outputTokens: stepResult.usage?.completion_tokens ?? 0,
|
|
2211
|
+
...(stepResult.usage?.prompt_tokens_details?.cached_tokens !==
|
|
2212
|
+
undefined
|
|
2213
|
+
? {
|
|
2214
|
+
cacheReadTokens: Math.min(stepResult.usage.prompt_tokens_details.cached_tokens, stepResult.usage.prompt_tokens ?? 0),
|
|
2215
|
+
}
|
|
2216
|
+
: {}),
|
|
2217
|
+
}));
|
|
2145
2218
|
}
|
|
2146
2219
|
resolveUsage(toDeferredUsage());
|
|
2147
2220
|
resolveFinish(stepFinish ?? "stop");
|
|
@@ -2258,7 +2331,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
2258
2331
|
});
|
|
2259
2332
|
}
|
|
2260
2333
|
async executeToolBatch(args) {
|
|
2261
|
-
const { stepResult, conversation, toolsRecord, toolNameFromWire,
|
|
2334
|
+
const { stepResult, conversation, toolsRecord, toolNameFromWire, toolsUsed, toolExecutionSummaries, options, stepIndex, } = args;
|
|
2262
2335
|
const toolCallsForMessage = [];
|
|
2263
2336
|
for (const [, t] of stepResult.toolCalls) {
|
|
2264
2337
|
toolCallsForMessage.push({
|
|
@@ -2295,14 +2368,14 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
2295
2368
|
// loaded via search_tools — that's a real tool, not a hallucination.
|
|
2296
2369
|
const toolDef = toolsRecord[registryName] ??
|
|
2297
2370
|
resolveDeferredTool(toolsRecord, registryName);
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
input,
|
|
2302
|
-
});
|
|
2371
|
+
// No `tool:start` here either: registered and built-in tools emit their
|
|
2372
|
+
// own start/end pair from the executor that runs them, and this raw
|
|
2373
|
+
// emission made every one of them count twice.
|
|
2303
2374
|
if (!toolDef || typeof toolDef.execute !== "function") {
|
|
2304
2375
|
errorMsg = `Tool '${registryName}' is not registered.`;
|
|
2305
2376
|
output = { error: errorMsg };
|
|
2377
|
+
// Rejected before any execute ran, so no executor will report it.
|
|
2378
|
+
this.emitRejectedToolCall(registryName, errorMsg, t.id);
|
|
2306
2379
|
}
|
|
2307
2380
|
else {
|
|
2308
2381
|
try {
|
|
@@ -2323,6 +2396,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
2323
2396
|
...(errorMsg ? { error: errorMsg } : {}),
|
|
2324
2397
|
startTime: startedAt,
|
|
2325
2398
|
endTime: endedAt,
|
|
2399
|
+
stepIndex,
|
|
2326
2400
|
});
|
|
2327
2401
|
conversation.push({
|
|
2328
2402
|
role: "tool",
|
|
@@ -2331,21 +2405,27 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
2331
2405
|
});
|
|
2332
2406
|
}
|
|
2333
2407
|
const justExecuted = toolExecutionSummaries.slice(-stepResult.toolCalls.size);
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
...(s.error ? { error: s.error } : {}),
|
|
2338
|
-
})));
|
|
2408
|
+
// No `tool:end` here: the executor already emitted start/end with the
|
|
2409
|
+
// real duration, and a second emission per result doubled every
|
|
2410
|
+
// consumer's count.
|
|
2339
2411
|
try {
|
|
2412
|
+
// `args`, not `input`: both stores read `args`, so the previous
|
|
2413
|
+
// `input:` key persisted every tool call with empty arguments.
|
|
2340
2414
|
await this.handleToolExecutionStorage(justExecuted.map((s) => ({
|
|
2341
2415
|
toolCallId: s.toolCallId,
|
|
2342
2416
|
toolName: s.toolName,
|
|
2343
|
-
|
|
2344
|
-
|
|
2417
|
+
args: s.input !== null &&
|
|
2418
|
+
typeof s.input === "object" &&
|
|
2419
|
+
!Array.isArray(s.input)
|
|
2420
|
+
? s.input
|
|
2421
|
+
: {},
|
|
2422
|
+
...(s.stepIndex !== undefined ? { stepIndex: s.stepIndex } : {}),
|
|
2345
2423
|
})), justExecuted.map((s) => ({
|
|
2346
2424
|
toolCallId: s.toolCallId,
|
|
2347
2425
|
toolName: s.toolName,
|
|
2348
2426
|
output: s.output,
|
|
2427
|
+
...(s.error ? { error: s.error } : {}),
|
|
2428
|
+
...(s.stepIndex !== undefined ? { stepIndex: s.stepIndex } : {}),
|
|
2349
2429
|
})), options, new Date());
|
|
2350
2430
|
}
|
|
2351
2431
|
catch (err) {
|
|
@@ -133,6 +133,12 @@ export declare abstract class SystemOneDecisionProvider extends BaseProvider {
|
|
|
133
133
|
* Refuse a request the model cannot read in full. An encoder cuts the state
|
|
134
134
|
* off past its window without saying so, so without this a decision would
|
|
135
135
|
* be made on input the model never saw — and reported as if it had.
|
|
136
|
+
*
|
|
137
|
+
* Advisory limits (`decisionLimits.advisory`) are skipped: they publish a
|
|
138
|
+
* server's own ceiling for `NeuroLink.decisionLimits()` to report, and the
|
|
139
|
+
* server enforces them itself. The estimator and the flattening are the
|
|
140
|
+
* same functions that method uses, so a host that sizes a state against the
|
|
141
|
+
* reading measures exactly what is checked here.
|
|
136
142
|
*/
|
|
137
143
|
private assertWithinDecisionLimits;
|
|
138
144
|
/**
|
|
@@ -6,7 +6,7 @@ import { ProviderError } from "../types/index.js";
|
|
|
6
6
|
import { prepareDecisionMedia } from "../utils/decisionMedia.js";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
8
|
import { redactUrlsInText } from "../utils/logSanitize.js";
|
|
9
|
-
import {
|
|
9
|
+
import { estimateDecisionStateTokens, resolveDecisionLimitsReading, } from "../utils/decisionLimits.js";
|
|
10
10
|
/**
|
|
11
11
|
* Generous enough for a cold start (measured at 2.0–2.7s after idle on Jev)
|
|
12
12
|
* while still bounded. Every internal caller is fail-open, so this only bites
|
|
@@ -171,59 +171,6 @@ const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
|
171
171
|
}
|
|
172
172
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
173
173
|
});
|
|
174
|
-
/**
|
|
175
|
-
* `estimateTokens` assumes ~4 characters per token. That holds for English
|
|
176
|
-
* and is several times too generous for other scripts on an English
|
|
177
|
-
* tokenizer, so when a provider declares a rate, non-ASCII characters are
|
|
178
|
-
* counted at it instead. ASCII text is estimated exactly as before.
|
|
179
|
-
*/
|
|
180
|
-
function estimateDecisionStateTokens(text, nonAsciiTokensPerChar, rates = {}) {
|
|
181
|
-
const { digit, symbol, astral } = rates;
|
|
182
|
-
if (nonAsciiTokensPerChar === undefined &&
|
|
183
|
-
digit === undefined &&
|
|
184
|
-
symbol === undefined &&
|
|
185
|
-
astral === undefined) {
|
|
186
|
-
return estimateTokens(text);
|
|
187
|
-
}
|
|
188
|
-
// Each class of character is counted once, at its own rate. A class with no
|
|
189
|
-
// declared rate stays in the ordinary four-characters-a-token estimate (or,
|
|
190
|
-
// for non-ASCII, at that estimate's per-character rate), so a provider that
|
|
191
|
-
// declares none of the extra rates is estimated exactly as before.
|
|
192
|
-
const isDigit = (c) => c >= "0" && c <= "9";
|
|
193
|
-
const isSymbol = (c) => /[!-/:-@[-`{-~]/.test(c);
|
|
194
|
-
let digits = 0;
|
|
195
|
-
let symbols = 0;
|
|
196
|
-
let astrals = 0;
|
|
197
|
-
let nonAscii = 0;
|
|
198
|
-
const rest = [];
|
|
199
|
-
for (const c of text) {
|
|
200
|
-
const code = c.codePointAt(0) ?? 0;
|
|
201
|
-
if (code > 0xffff && astral !== undefined) {
|
|
202
|
-
astrals += 1;
|
|
203
|
-
}
|
|
204
|
-
else if (code > 0x7f) {
|
|
205
|
-
nonAscii += 1;
|
|
206
|
-
}
|
|
207
|
-
else if (digit !== undefined && isDigit(c)) {
|
|
208
|
-
digits += 1;
|
|
209
|
-
}
|
|
210
|
-
else if (symbol !== undefined && isSymbol(c)) {
|
|
211
|
-
symbols += 1;
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
rest.push(c);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
// Digits, punctuation and astral characters (emoji) are charged separately
|
|
218
|
-
// only when the provider declares a rate for them: a tokenizer that reads each
|
|
219
|
-
// digit, and most punctuation, on its own makes a number-heavy or JSON-heavy
|
|
220
|
-
// state several times longer than four characters a token suggests.
|
|
221
|
-
return (estimateTokens(rest.join("")) +
|
|
222
|
-
Math.ceil(digits * (digit ?? 0)) +
|
|
223
|
-
Math.ceil(symbols * (symbol ?? 0)) +
|
|
224
|
-
Math.ceil(astrals * (astral ?? 0)) +
|
|
225
|
-
Math.ceil(nonAscii * (nonAsciiTokensPerChar ?? 1 / CHARS_PER_TOKEN)));
|
|
226
|
-
}
|
|
227
174
|
/**
|
|
228
175
|
* The shared half of every "System One" decision provider — a model that takes
|
|
229
176
|
* one `state` plus typed questions and returns one typed answer each, with no
|
|
@@ -551,10 +498,19 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
551
498
|
* Refuse a request the model cannot read in full. An encoder cuts the state
|
|
552
499
|
* off past its window without saying so, so without this a decision would
|
|
553
500
|
* be made on input the model never saw — and reported as if it had.
|
|
501
|
+
*
|
|
502
|
+
* Advisory limits (`decisionLimits.advisory`) are skipped: they publish a
|
|
503
|
+
* server's own ceiling for `NeuroLink.decisionLimits()` to report, and the
|
|
504
|
+
* server enforces them itself. The estimator and the flattening are the
|
|
505
|
+
* same functions that method uses, so a host that sizes a state against the
|
|
506
|
+
* reading measures exactly what is checked here.
|
|
554
507
|
*/
|
|
555
508
|
assertWithinDecisionLimits(request, model, questionCount) {
|
|
556
|
-
const
|
|
557
|
-
|
|
509
|
+
const descriptor = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName);
|
|
510
|
+
const limits = descriptor
|
|
511
|
+
? resolveDecisionLimitsReading(descriptor, model)
|
|
512
|
+
: null;
|
|
513
|
+
if (!limits || !limits.enforcedLocally) {
|
|
558
514
|
return;
|
|
559
515
|
}
|
|
560
516
|
const label = this.vendorLabel();
|
|
@@ -566,13 +522,8 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
566
522
|
retryable: false,
|
|
567
523
|
});
|
|
568
524
|
}
|
|
569
|
-
const
|
|
570
|
-
const
|
|
571
|
-
const stateTokens = estimateDecisionStateTokens(serializeForEstimate(request.state), modelLimits?.nonAsciiTokensPerChar ?? limits.nonAsciiTokensPerChar, {
|
|
572
|
-
digit: limits.digitTokensPerChar,
|
|
573
|
-
symbol: limits.symbolTokensPerChar,
|
|
574
|
-
astral: limits.astralTokensPerChar,
|
|
575
|
-
});
|
|
525
|
+
const { maxStateTokens } = limits;
|
|
526
|
+
const stateTokens = estimateDecisionStateTokens(request.state, limits);
|
|
576
527
|
if (stateTokens > maxStateTokens) {
|
|
577
528
|
throw this.decisionError({
|
|
578
529
|
kind: "max_tokens_exceeded",
|
|
@@ -4,6 +4,12 @@ import { SystemOneDecisionProvider } from "./systemOneDecision.js";
|
|
|
4
4
|
* The binding limit in practice: `state` plus the SINGLE LONGEST question.
|
|
5
5
|
* Measured by bisection at single-character resolution — 33,002 reported
|
|
6
6
|
* input tokens accepted, 33,003 not.
|
|
7
|
+
*
|
|
8
|
+
* The same figure sits on the TypeSafe descriptor as an ADVISORY
|
|
9
|
+
* `decisionLimits.maxStateTokens`, which is what `NeuroLink.decisionLimits()`
|
|
10
|
+
* reads; the server enforces it, NeuroLink only reports it. The descriptor
|
|
11
|
+
* cannot import this constant without a cycle, so the decide suite pins them
|
|
12
|
+
* equal instead.
|
|
7
13
|
*/
|
|
8
14
|
export declare const TYPESAFE_MAX_STATE_TOKENS = 33000;
|
|
9
15
|
/**
|
|
@@ -27,6 +27,12 @@ const GATEWAY_SPEC_VERSION = "4";
|
|
|
27
27
|
* The binding limit in practice: `state` plus the SINGLE LONGEST question.
|
|
28
28
|
* Measured by bisection at single-character resolution — 33,002 reported
|
|
29
29
|
* input tokens accepted, 33,003 not.
|
|
30
|
+
*
|
|
31
|
+
* The same figure sits on the TypeSafe descriptor as an ADVISORY
|
|
32
|
+
* `decisionLimits.maxStateTokens`, which is what `NeuroLink.decisionLimits()`
|
|
33
|
+
* reads; the server enforces it, NeuroLink only reports it. The descriptor
|
|
34
|
+
* cannot import this constant without a cycle, so the decide suite pins them
|
|
35
|
+
* equal instead.
|
|
30
36
|
*/
|
|
31
37
|
export const TYPESAFE_MAX_STATE_TOKENS = 33_000;
|
|
32
38
|
/**
|
|
@@ -75,6 +75,7 @@ export async function decideSearchPlan(query, decide, capabilities) {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
const result = await decide({
|
|
78
|
+
site: "ragPlan",
|
|
78
79
|
state: { query: query.slice(0, 4000) },
|
|
79
80
|
// The map is assembled dynamically above; its members are all valid
|
|
80
81
|
// question shapes, which the cast asserts once here rather than at four
|
|
@@ -98,7 +98,7 @@ export class ClassifierRouter {
|
|
|
98
98
|
try {
|
|
99
99
|
// Both the LLM and Jev strategies can pick a model straight out of the
|
|
100
100
|
// pool — the generic path for custom/registry-less models.
|
|
101
|
-
const strategy = this.resolveStrategy();
|
|
101
|
+
const strategy = this.resolveStrategy(input);
|
|
102
102
|
const usesCandidates = (strategy === "jev" && !!this.deps.decide) ||
|
|
103
103
|
(strategy === "llm" && !!this.deps.generate);
|
|
104
104
|
const built = usesCandidates ? this.buildCandidates() : undefined;
|
|
@@ -138,7 +138,7 @@ export class ClassifierRouter {
|
|
|
138
138
|
* moment a decision provider is configured; without one the behaviour is
|
|
139
139
|
* exactly what it was before Jev existed.
|
|
140
140
|
*/
|
|
141
|
-
resolveStrategy() {
|
|
141
|
+
resolveStrategy(input) {
|
|
142
142
|
const configured = this.config.classifier ?? "auto";
|
|
143
143
|
if (configured !== "auto") {
|
|
144
144
|
return configured;
|
|
@@ -146,10 +146,11 @@ export class ClassifierRouter {
|
|
|
146
146
|
// A decision provider that is both registered AND configured is the whole
|
|
147
147
|
// activation condition — this is where "if somebody sets it we start
|
|
148
148
|
// using it" lives for routing. The caller's check also counts the
|
|
149
|
-
// credentials it was constructed with
|
|
149
|
+
// credentials it was constructed with and the request's own per-call
|
|
150
|
+
// credentials, which the decision call itself will use.
|
|
150
151
|
const hasProvider = this.deps.hasDecisionProvider
|
|
151
|
-
? this.deps.hasDecisionProvider()
|
|
152
|
-
: resolveDefaultDecisionProvider() !== undefined;
|
|
152
|
+
? this.deps.hasDecisionProvider(input.credentials)
|
|
153
|
+
: resolveDefaultDecisionProvider(input.credentials) !== undefined;
|
|
153
154
|
return this.deps.decide && hasProvider ? "jev" : "heuristic";
|
|
154
155
|
}
|
|
155
156
|
/** Run the chosen strategy; every one falls back to heuristic on failure. */
|
|
@@ -297,8 +297,15 @@ export async function classifyJev(input, decide, timeoutMs, candidates, threshol
|
|
|
297
297
|
// short conversational one.
|
|
298
298
|
// Continuity is sent as `session_bound` / `prior_messages`, never as the
|
|
299
299
|
// raw sessionId: the id is an opaque identifier that tells a third-party
|
|
300
|
-
// model nothing, so it stays in-process
|
|
300
|
+
// model nothing, so it stays in-process — as do the site stamp, the
|
|
301
|
+
// per-call credentials and the ids below, which the caller reads and the
|
|
302
|
+
// wire never sees.
|
|
301
303
|
const result = await decide({
|
|
304
|
+
site: "routing",
|
|
305
|
+
credentials: input.credentials,
|
|
306
|
+
signal: input.signal,
|
|
307
|
+
sessionId: input.sessionId,
|
|
308
|
+
requestId: input.requestId,
|
|
302
309
|
state: {
|
|
303
310
|
request: (input.prompt ?? "").slice(0, 8000),
|
|
304
311
|
estimated_input_tokens: input.estimatedInputTokens ?? 0,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type { ValidationSchema } from "./aliases.js";
|
|
17
17
|
import type { DecisionCallerFn } from "./decision.js";
|
|
18
|
+
import type { NeurolinkCredentials } from "./providers.js";
|
|
18
19
|
/** Coarse difficulty buckets the classifier maps a request into. */
|
|
19
20
|
export type ClassifierDifficulty = "trivial" | "simple" | "moderate" | "hard" | "expert";
|
|
20
21
|
/**
|
|
@@ -307,6 +308,16 @@ export type ClassifierRouterInput = {
|
|
|
307
308
|
sessionBound?: boolean;
|
|
308
309
|
/** Number of prior conversation messages the caller supplied, when known. */
|
|
309
310
|
priorMessageCount?: number;
|
|
311
|
+
/**
|
|
312
|
+
* The outer request's per-call credentials, forwarded to the decision call
|
|
313
|
+
* so it reaches the caller's provider account rather than the instance's.
|
|
314
|
+
* Never part of the state sent to the model.
|
|
315
|
+
*/
|
|
316
|
+
credentials?: NeurolinkCredentials;
|
|
317
|
+
/** The outer request's abort signal; an abandoned turn abandons its routing call. */
|
|
318
|
+
signal?: AbortSignal;
|
|
319
|
+
/** The outer request's id, for hook and event correlation. Never sent to the model. */
|
|
320
|
+
requestId?: string;
|
|
310
321
|
};
|
|
311
322
|
/** Enriched per-model metadata used while ranking pool members. */
|
|
312
323
|
export type ClassifierModelMeta = {
|
|
@@ -359,9 +370,11 @@ export type ClassifierRouterDeps = {
|
|
|
359
370
|
decide?: ClassifierDecideFn;
|
|
360
371
|
/**
|
|
361
372
|
* Whether a decision provider is configured for this caller, counting the
|
|
362
|
-
* credentials it was given as well as the environment
|
|
363
|
-
*
|
|
373
|
+
* credentials it was given as well as the environment — and the request's
|
|
374
|
+
* own per-call `credentials`, passed in from `ClassifierRouterInput`, so a
|
|
375
|
+
* caller that holds no decision key itself can still route a request that
|
|
376
|
+
* carries one. Omit to check the environment alone.
|
|
364
377
|
*/
|
|
365
|
-
hasDecisionProvider?: () => boolean;
|
|
378
|
+
hasDecisionProvider?: (callCredentials?: NeurolinkCredentials) => boolean;
|
|
366
379
|
logger?: ClassifierLogger;
|
|
367
380
|
};
|
package/dist/types/common.d.ts
CHANGED
|
@@ -102,6 +102,8 @@ export type NeuroLinkEvents = {
|
|
|
102
102
|
"stream:error": unknown;
|
|
103
103
|
"generation:start": unknown;
|
|
104
104
|
"generation:end": unknown;
|
|
105
|
+
"decision:before": unknown;
|
|
106
|
+
"decision:after": unknown;
|
|
105
107
|
"response:start": unknown;
|
|
106
108
|
"response:end": unknown;
|
|
107
109
|
"externalMCP:serverConnected": unknown;
|
package/dist/types/config.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { ModelPoolConfig } from "./modelPool.js";
|
|
|
14
14
|
import type { RequestRouter } from "./requestRouter.js";
|
|
15
15
|
import type { ClassifierRouterConfig } from "./classifierRouter.js";
|
|
16
16
|
import type { ContextRelevanceOptions } from "./context.js";
|
|
17
|
+
import type { DecisionHooks } from "./decision.js";
|
|
17
18
|
import type { SkillsConfig } from "./skills.js";
|
|
18
19
|
import type { KnowledgeGroundingConfig } from "./knowledge.js";
|
|
19
20
|
/**
|
|
@@ -155,6 +156,14 @@ export type NeurolinkConstructorConfig = {
|
|
|
155
156
|
* which is the same activation rule every other decision consumer uses.
|
|
156
157
|
*/
|
|
157
158
|
contextRelevance?: ContextRelevanceOptions;
|
|
159
|
+
/**
|
|
160
|
+
* Ride along on the decision calls NeuroLink already makes for routing,
|
|
161
|
+
* tool routing, compaction and RAG planning: add your own questions to the
|
|
162
|
+
* same round trip and read the answers back. Observe-only with respect to
|
|
163
|
+
* NeuroLink's behaviour, fail-open, and inert until a decision provider is
|
|
164
|
+
* configured. See {@link DecisionHooks}.
|
|
165
|
+
*/
|
|
166
|
+
decisionHooks?: DecisionHooks;
|
|
158
167
|
/**
|
|
159
168
|
* Native skills: versioned, discoverable instruction packs (SOPs,
|
|
160
169
|
* playbooks) with progressive disclosure. When enabled, each
|
package/dist/types/context.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { ExecutionContext } from "../types/tools.js";
|
|
6
6
|
import type { JsonObject } from "./common.js";
|
|
7
|
-
import type { ChatMessage, ConversationMemoryConfig } from "./conversation.js";
|
|
7
|
+
import type { ChatMessage, ConversationMemoryConfig, ToolReplayMode } from "./conversation.js";
|
|
8
8
|
import type { DecisionCallerFn } from "./decision.js";
|
|
9
9
|
/**
|
|
10
10
|
* Base context type for all AI operations
|
|
@@ -237,6 +237,13 @@ export type BudgetCheckParams = {
|
|
|
237
237
|
}>;
|
|
238
238
|
/** Compaction trigger threshold (0.0-1.0). Default: 0.80 */
|
|
239
239
|
compactionThreshold?: number;
|
|
240
|
+
/**
|
|
241
|
+
* How this request replays stored `tool_call` / `tool_result` rows
|
|
242
|
+
* (`replayToolSteps`). When set, tool rows are estimated at the size they
|
|
243
|
+
* reach the model in that mode rather than at stored size; when omitted
|
|
244
|
+
* they count at stored size.
|
|
245
|
+
*/
|
|
246
|
+
toolReplayMode?: ToolReplayMode;
|
|
240
247
|
};
|
|
241
248
|
/** A file prepared for potential summarization. */
|
|
242
249
|
export type FileForSummarization = {
|