@juspay/neurolink 11.9.0 → 11.10.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 +2 -2
- package/dist/browser/neurolink.min.js +1 -1
- package/dist/core/geminiLoopAdapter.d.ts +20 -0
- package/dist/core/geminiLoopAdapter.js +170 -0
- package/dist/lib/core/geminiLoopAdapter.d.ts +20 -0
- package/dist/lib/core/geminiLoopAdapter.js +171 -0
- package/dist/lib/providers/googleNativeGemini3/utils.js +9 -0
- package/dist/lib/types/loopEngine.d.ts +89 -0
- package/dist/lib/types/providers.d.ts +2 -0
- package/dist/providers/googleNativeGemini3/utils.js +9 -0
- package/dist/types/loopEngine.d.ts +89 -0
- package/dist/types/providers.d.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Gemini adapter onto the agentic loop engine.
|
|
3
|
+
*
|
|
4
|
+
* Lives in `core/` rather than inside either provider's folder because two
|
|
5
|
+
* providers use it: Google AI Studio (Task 9) and Vertex Gemini (Task 10).
|
|
6
|
+
* Both issue the same wire call — `models.generateContentStream` — and consume
|
|
7
|
+
* the same response shape, so one `executeStep` serves four hand-rolled loops
|
|
8
|
+
* (each provider has a streaming one and a near-duplicate inside `generate()`).
|
|
9
|
+
*
|
|
10
|
+
* Function-name sanitization stays entirely on this side of the engine
|
|
11
|
+
* boundary, which is the design decision Task 7 recorded rather than a
|
|
12
|
+
* shortcut. Google requires function names to match a restricted pattern, so
|
|
13
|
+
* declarations are built with sanitized names and the model calls back with
|
|
14
|
+
* those same sanitized names. The engine only ever sees plain string names, so
|
|
15
|
+
* this adapter translates sanitized -> original before returning `toolCalls`,
|
|
16
|
+
* and original -> sanitized when writing `functionResponse` parts. Neither the
|
|
17
|
+
* engine nor any other adapter needs to know sanitization happened.
|
|
18
|
+
*/
|
|
19
|
+
import type { AgenticLoopAdapter, GeminiLoopAdapterConfig, GeminiStepRaw, GeminiTurnContent } from "../types/index.js";
|
|
20
|
+
export declare function createGeminiLoopAdapter(config: GeminiLoopAdapterConfig): AgenticLoopAdapter<GeminiTurnContent[], GeminiStepRaw>;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Gemini adapter onto the agentic loop engine.
|
|
3
|
+
*
|
|
4
|
+
* Lives in `core/` rather than inside either provider's folder because two
|
|
5
|
+
* providers use it: Google AI Studio (Task 9) and Vertex Gemini (Task 10).
|
|
6
|
+
* Both issue the same wire call — `models.generateContentStream` — and consume
|
|
7
|
+
* the same response shape, so one `executeStep` serves four hand-rolled loops
|
|
8
|
+
* (each provider has a streaming one and a near-duplicate inside `generate()`).
|
|
9
|
+
*
|
|
10
|
+
* Function-name sanitization stays entirely on this side of the engine
|
|
11
|
+
* boundary, which is the design decision Task 7 recorded rather than a
|
|
12
|
+
* shortcut. Google requires function names to match a restricted pattern, so
|
|
13
|
+
* declarations are built with sanitized names and the model calls back with
|
|
14
|
+
* those same sanitized names. The engine only ever sees plain string names, so
|
|
15
|
+
* this adapter translates sanitized -> original before returning `toolCalls`,
|
|
16
|
+
* and original -> sanitized when writing `functionResponse` parts. Neither the
|
|
17
|
+
* engine nor any other adapter needs to know sanitization happened.
|
|
18
|
+
*/
|
|
19
|
+
import { collectStreamChunksIncremental, extractTextFromParts, mapGeminiFinishReason, pushModelResponseToHistory, refreshNativeToolDeclarations, } from "../providers/googleNativeGemini3/utils.js";
|
|
20
|
+
export function createGeminiLoopAdapter(config) {
|
|
21
|
+
/**
|
|
22
|
+
* Sanitized wire name -> the name the caller registered. Rebuilt per step
|
|
23
|
+
* because mid-turn discovery can add entries between steps.
|
|
24
|
+
*/
|
|
25
|
+
const originalNameFor = (sanitized) => config.declarations?.originalNameMap?.get(sanitized) ?? sanitized;
|
|
26
|
+
const sanitizedNameFor = (original) => {
|
|
27
|
+
const map = config.declarations?.originalNameMap;
|
|
28
|
+
if (!map) {
|
|
29
|
+
return original;
|
|
30
|
+
}
|
|
31
|
+
for (const [sanitized, name] of map) {
|
|
32
|
+
if (name === original) {
|
|
33
|
+
return sanitized;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return original;
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
providerLabel: config.providerLabel,
|
|
40
|
+
maxSteps: config.maxSteps,
|
|
41
|
+
...(config.toolFailureBreaker
|
|
42
|
+
? { toolFailureBreaker: config.toolFailureBreaker }
|
|
43
|
+
: {}),
|
|
44
|
+
/**
|
|
45
|
+
* Mid-turn discovery: `search_tools` hydrates new tools into the live
|
|
46
|
+
* record between steps, and Gemini only calls what the request declared,
|
|
47
|
+
* so the declarations are refreshed before each step is built. This is
|
|
48
|
+
* why `resolveToolOnMiss` exists as well — a tool discovered during a
|
|
49
|
+
* step is callable in the same step, before the next refresh.
|
|
50
|
+
*/
|
|
51
|
+
buildStepRequest(conversation, step) {
|
|
52
|
+
if (config.declarations) {
|
|
53
|
+
refreshNativeToolDeclarations(config.liveTools, config.declarations);
|
|
54
|
+
}
|
|
55
|
+
return { raw: config.buildRequest(conversation, step) };
|
|
56
|
+
},
|
|
57
|
+
/**
|
|
58
|
+
* The engine decides WHEN to reclaim; the provider decides HOW. Wiring
|
|
59
|
+
* this is not optional dressing: both loops append a model turn and a
|
|
60
|
+
* tool turn every step, so without it a long agentic run overflows the
|
|
61
|
+
* context window mid-turn and loses the work already done.
|
|
62
|
+
*/
|
|
63
|
+
...(config.planReclaim
|
|
64
|
+
? {
|
|
65
|
+
planReclaim: (conversation, step) => {
|
|
66
|
+
const reclaimed = config.planReclaim?.(conversation, step);
|
|
67
|
+
return reclaimed ? { conversation: reclaimed } : undefined;
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
: {}),
|
|
71
|
+
/**
|
|
72
|
+
* A step that produced no text, no function calls, and a
|
|
73
|
+
* MALFORMED_FUNCTION_CALL finish reason is a transient formatting
|
|
74
|
+
* failure, not a finished turn. Vertex retries it once with a corrective
|
|
75
|
+
* note rather than hard-ending on empty content — automated RCA turns
|
|
76
|
+
* were dying at step 2-4 on this and being mislabelled as step-cap exits.
|
|
77
|
+
*
|
|
78
|
+
* Opt-in: AI Studio has no such retry today and gaining one silently
|
|
79
|
+
* would be a behaviour change, not a migration.
|
|
80
|
+
*/
|
|
81
|
+
...(config.enableMalformedRetry && config.buildMalformedRetryNote
|
|
82
|
+
? {
|
|
83
|
+
isMalformedStep: (stepResult) => stepResult.toolCalls.length === 0 &&
|
|
84
|
+
!stepResult.text &&
|
|
85
|
+
stepResult.rawStopReason === "MALFORMED_FUNCTION_CALL",
|
|
86
|
+
buildMalformedRetryNote: config.buildMalformedRetryNote,
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
89
|
+
resolveToolOnMiss: (name) => {
|
|
90
|
+
const tool = config.liveTools?.[name];
|
|
91
|
+
const execute = tool?.execute;
|
|
92
|
+
if (!execute) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
// Wrapped because the hook types `opts` as `unknown`, and a function
|
|
96
|
+
// declaring a narrower options type is not assignable to one accepting
|
|
97
|
+
// `unknown`. One assertion at the boundary, never a double assertion.
|
|
98
|
+
return {
|
|
99
|
+
execute: async (args, opts) => execute(args, opts),
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
async executeStep(request, channel, signal) {
|
|
103
|
+
const rawStream = await config.sendStep(request.raw, signal);
|
|
104
|
+
// `collectStreamChunksIncremental` wants a StreamChannel but only ever
|
|
105
|
+
// calls `.push`, so the engine's push-only channel satisfies it. The
|
|
106
|
+
// helper is reused rather than reimplemented precisely because it also
|
|
107
|
+
// owns usage extraction and thought-signature preservation.
|
|
108
|
+
const collected = await collectStreamChunksIncremental(rawStream, {
|
|
109
|
+
push: (chunk) => channel.push(chunk),
|
|
110
|
+
});
|
|
111
|
+
// The provider's context guard calibrates from real per-step counts;
|
|
112
|
+
// `inputTokens` is this step's full prompt size, which is what it
|
|
113
|
+
// projects the next request from.
|
|
114
|
+
config.noteUsage?.(collected.inputTokens, collected.outputTokens);
|
|
115
|
+
const text = extractTextFromParts(collected.rawResponseParts);
|
|
116
|
+
// Names cross the engine boundary in their ORIGINAL form.
|
|
117
|
+
const toolCalls = collected.stepFunctionCalls.map((call, index) => ({
|
|
118
|
+
id: `${config.providerLabel}_${index}_${call.name}`,
|
|
119
|
+
name: originalNameFor(call.name),
|
|
120
|
+
args: call.args,
|
|
121
|
+
}));
|
|
122
|
+
return {
|
|
123
|
+
text,
|
|
124
|
+
toolCalls,
|
|
125
|
+
usage: {
|
|
126
|
+
inputTokens: collected.inputTokens,
|
|
127
|
+
outputTokens: collected.outputTokens,
|
|
128
|
+
...(collected.cacheReadTokens
|
|
129
|
+
? { cacheReadTokens: collected.cacheReadTokens }
|
|
130
|
+
: {}),
|
|
131
|
+
...(collected.reasoningTokens
|
|
132
|
+
? { reasoningTokens: collected.reasoningTokens }
|
|
133
|
+
: {}),
|
|
134
|
+
},
|
|
135
|
+
rawStopReason: collected.finishReason,
|
|
136
|
+
raw: {
|
|
137
|
+
rawResponseParts: collected.rawResponseParts,
|
|
138
|
+
stepFunctionCalls: collected.stepFunctionCalls,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
buildToolResultMessages(conversation, stepResult, toolResults) {
|
|
143
|
+
// Copied before `pushModelResponseToHistory` mutates it: the engine
|
|
144
|
+
// treats the conversation as a value it hands in and gets back, so
|
|
145
|
+
// mutating the caller's array in place would make a retried or
|
|
146
|
+
// reclaimed step see history it should not.
|
|
147
|
+
const next = [...conversation];
|
|
148
|
+
pushModelResponseToHistory(next, stepResult.raw.rawResponseParts, stepResult.raw.stepFunctionCalls);
|
|
149
|
+
next.push({
|
|
150
|
+
role: "user",
|
|
151
|
+
parts: toolResults.map((result) => ({
|
|
152
|
+
functionResponse: {
|
|
153
|
+
// Back to the sanitized wire name the model actually called.
|
|
154
|
+
name: sanitizedNameFor(result.name),
|
|
155
|
+
response: result.error
|
|
156
|
+
? { error: result.error }
|
|
157
|
+
: { result: result.output },
|
|
158
|
+
},
|
|
159
|
+
})),
|
|
160
|
+
});
|
|
161
|
+
return next;
|
|
162
|
+
},
|
|
163
|
+
mapFinishReason(rawStopReason, hadToolCallsAtCap) {
|
|
164
|
+
const mapped = mapGeminiFinishReason(rawStopReason);
|
|
165
|
+
// A turn cut off at the step cap ended because of the cap, not because
|
|
166
|
+
// the last response said "STOP".
|
|
167
|
+
return hadToolCallsAtCap && mapped === "stop" ? "tool-calls" : mapped;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Gemini adapter onto the agentic loop engine.
|
|
3
|
+
*
|
|
4
|
+
* Lives in `core/` rather than inside either provider's folder because two
|
|
5
|
+
* providers use it: Google AI Studio (Task 9) and Vertex Gemini (Task 10).
|
|
6
|
+
* Both issue the same wire call — `models.generateContentStream` — and consume
|
|
7
|
+
* the same response shape, so one `executeStep` serves four hand-rolled loops
|
|
8
|
+
* (each provider has a streaming one and a near-duplicate inside `generate()`).
|
|
9
|
+
*
|
|
10
|
+
* Function-name sanitization stays entirely on this side of the engine
|
|
11
|
+
* boundary, which is the design decision Task 7 recorded rather than a
|
|
12
|
+
* shortcut. Google requires function names to match a restricted pattern, so
|
|
13
|
+
* declarations are built with sanitized names and the model calls back with
|
|
14
|
+
* those same sanitized names. The engine only ever sees plain string names, so
|
|
15
|
+
* this adapter translates sanitized -> original before returning `toolCalls`,
|
|
16
|
+
* and original -> sanitized when writing `functionResponse` parts. Neither the
|
|
17
|
+
* engine nor any other adapter needs to know sanitization happened.
|
|
18
|
+
*/
|
|
19
|
+
import type { AgenticLoopAdapter, GeminiLoopAdapterConfig, GeminiStepRaw, GeminiTurnContent } from "../types/index.js";
|
|
20
|
+
export declare function createGeminiLoopAdapter(config: GeminiLoopAdapterConfig): AgenticLoopAdapter<GeminiTurnContent[], GeminiStepRaw>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Gemini adapter onto the agentic loop engine.
|
|
3
|
+
*
|
|
4
|
+
* Lives in `core/` rather than inside either provider's folder because two
|
|
5
|
+
* providers use it: Google AI Studio (Task 9) and Vertex Gemini (Task 10).
|
|
6
|
+
* Both issue the same wire call — `models.generateContentStream` — and consume
|
|
7
|
+
* the same response shape, so one `executeStep` serves four hand-rolled loops
|
|
8
|
+
* (each provider has a streaming one and a near-duplicate inside `generate()`).
|
|
9
|
+
*
|
|
10
|
+
* Function-name sanitization stays entirely on this side of the engine
|
|
11
|
+
* boundary, which is the design decision Task 7 recorded rather than a
|
|
12
|
+
* shortcut. Google requires function names to match a restricted pattern, so
|
|
13
|
+
* declarations are built with sanitized names and the model calls back with
|
|
14
|
+
* those same sanitized names. The engine only ever sees plain string names, so
|
|
15
|
+
* this adapter translates sanitized -> original before returning `toolCalls`,
|
|
16
|
+
* and original -> sanitized when writing `functionResponse` parts. Neither the
|
|
17
|
+
* engine nor any other adapter needs to know sanitization happened.
|
|
18
|
+
*/
|
|
19
|
+
import { collectStreamChunksIncremental, extractTextFromParts, mapGeminiFinishReason, pushModelResponseToHistory, refreshNativeToolDeclarations, } from "../providers/googleNativeGemini3/utils.js";
|
|
20
|
+
export function createGeminiLoopAdapter(config) {
|
|
21
|
+
/**
|
|
22
|
+
* Sanitized wire name -> the name the caller registered. Rebuilt per step
|
|
23
|
+
* because mid-turn discovery can add entries between steps.
|
|
24
|
+
*/
|
|
25
|
+
const originalNameFor = (sanitized) => config.declarations?.originalNameMap?.get(sanitized) ?? sanitized;
|
|
26
|
+
const sanitizedNameFor = (original) => {
|
|
27
|
+
const map = config.declarations?.originalNameMap;
|
|
28
|
+
if (!map) {
|
|
29
|
+
return original;
|
|
30
|
+
}
|
|
31
|
+
for (const [sanitized, name] of map) {
|
|
32
|
+
if (name === original) {
|
|
33
|
+
return sanitized;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return original;
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
providerLabel: config.providerLabel,
|
|
40
|
+
maxSteps: config.maxSteps,
|
|
41
|
+
...(config.toolFailureBreaker
|
|
42
|
+
? { toolFailureBreaker: config.toolFailureBreaker }
|
|
43
|
+
: {}),
|
|
44
|
+
/**
|
|
45
|
+
* Mid-turn discovery: `search_tools` hydrates new tools into the live
|
|
46
|
+
* record between steps, and Gemini only calls what the request declared,
|
|
47
|
+
* so the declarations are refreshed before each step is built. This is
|
|
48
|
+
* why `resolveToolOnMiss` exists as well — a tool discovered during a
|
|
49
|
+
* step is callable in the same step, before the next refresh.
|
|
50
|
+
*/
|
|
51
|
+
buildStepRequest(conversation, step) {
|
|
52
|
+
if (config.declarations) {
|
|
53
|
+
refreshNativeToolDeclarations(config.liveTools, config.declarations);
|
|
54
|
+
}
|
|
55
|
+
return { raw: config.buildRequest(conversation, step) };
|
|
56
|
+
},
|
|
57
|
+
/**
|
|
58
|
+
* The engine decides WHEN to reclaim; the provider decides HOW. Wiring
|
|
59
|
+
* this is not optional dressing: both loops append a model turn and a
|
|
60
|
+
* tool turn every step, so without it a long agentic run overflows the
|
|
61
|
+
* context window mid-turn and loses the work already done.
|
|
62
|
+
*/
|
|
63
|
+
...(config.planReclaim
|
|
64
|
+
? {
|
|
65
|
+
planReclaim: (conversation, step) => {
|
|
66
|
+
const reclaimed = config.planReclaim?.(conversation, step);
|
|
67
|
+
return reclaimed ? { conversation: reclaimed } : undefined;
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
: {}),
|
|
71
|
+
/**
|
|
72
|
+
* A step that produced no text, no function calls, and a
|
|
73
|
+
* MALFORMED_FUNCTION_CALL finish reason is a transient formatting
|
|
74
|
+
* failure, not a finished turn. Vertex retries it once with a corrective
|
|
75
|
+
* note rather than hard-ending on empty content — automated RCA turns
|
|
76
|
+
* were dying at step 2-4 on this and being mislabelled as step-cap exits.
|
|
77
|
+
*
|
|
78
|
+
* Opt-in: AI Studio has no such retry today and gaining one silently
|
|
79
|
+
* would be a behaviour change, not a migration.
|
|
80
|
+
*/
|
|
81
|
+
...(config.enableMalformedRetry && config.buildMalformedRetryNote
|
|
82
|
+
? {
|
|
83
|
+
isMalformedStep: (stepResult) => stepResult.toolCalls.length === 0 &&
|
|
84
|
+
!stepResult.text &&
|
|
85
|
+
stepResult.rawStopReason === "MALFORMED_FUNCTION_CALL",
|
|
86
|
+
buildMalformedRetryNote: config.buildMalformedRetryNote,
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
89
|
+
resolveToolOnMiss: (name) => {
|
|
90
|
+
const tool = config.liveTools?.[name];
|
|
91
|
+
const execute = tool?.execute;
|
|
92
|
+
if (!execute) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
// Wrapped because the hook types `opts` as `unknown`, and a function
|
|
96
|
+
// declaring a narrower options type is not assignable to one accepting
|
|
97
|
+
// `unknown`. One assertion at the boundary, never a double assertion.
|
|
98
|
+
return {
|
|
99
|
+
execute: async (args, opts) => execute(args, opts),
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
async executeStep(request, channel, signal) {
|
|
103
|
+
const rawStream = await config.sendStep(request.raw, signal);
|
|
104
|
+
// `collectStreamChunksIncremental` wants a StreamChannel but only ever
|
|
105
|
+
// calls `.push`, so the engine's push-only channel satisfies it. The
|
|
106
|
+
// helper is reused rather than reimplemented precisely because it also
|
|
107
|
+
// owns usage extraction and thought-signature preservation.
|
|
108
|
+
const collected = await collectStreamChunksIncremental(rawStream, {
|
|
109
|
+
push: (chunk) => channel.push(chunk),
|
|
110
|
+
});
|
|
111
|
+
// The provider's context guard calibrates from real per-step counts;
|
|
112
|
+
// `inputTokens` is this step's full prompt size, which is what it
|
|
113
|
+
// projects the next request from.
|
|
114
|
+
config.noteUsage?.(collected.inputTokens, collected.outputTokens);
|
|
115
|
+
const text = extractTextFromParts(collected.rawResponseParts);
|
|
116
|
+
// Names cross the engine boundary in their ORIGINAL form.
|
|
117
|
+
const toolCalls = collected.stepFunctionCalls.map((call, index) => ({
|
|
118
|
+
id: `${config.providerLabel}_${index}_${call.name}`,
|
|
119
|
+
name: originalNameFor(call.name),
|
|
120
|
+
args: call.args,
|
|
121
|
+
}));
|
|
122
|
+
return {
|
|
123
|
+
text,
|
|
124
|
+
toolCalls,
|
|
125
|
+
usage: {
|
|
126
|
+
inputTokens: collected.inputTokens,
|
|
127
|
+
outputTokens: collected.outputTokens,
|
|
128
|
+
...(collected.cacheReadTokens
|
|
129
|
+
? { cacheReadTokens: collected.cacheReadTokens }
|
|
130
|
+
: {}),
|
|
131
|
+
...(collected.reasoningTokens
|
|
132
|
+
? { reasoningTokens: collected.reasoningTokens }
|
|
133
|
+
: {}),
|
|
134
|
+
},
|
|
135
|
+
rawStopReason: collected.finishReason,
|
|
136
|
+
raw: {
|
|
137
|
+
rawResponseParts: collected.rawResponseParts,
|
|
138
|
+
stepFunctionCalls: collected.stepFunctionCalls,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
buildToolResultMessages(conversation, stepResult, toolResults) {
|
|
143
|
+
// Copied before `pushModelResponseToHistory` mutates it: the engine
|
|
144
|
+
// treats the conversation as a value it hands in and gets back, so
|
|
145
|
+
// mutating the caller's array in place would make a retried or
|
|
146
|
+
// reclaimed step see history it should not.
|
|
147
|
+
const next = [...conversation];
|
|
148
|
+
pushModelResponseToHistory(next, stepResult.raw.rawResponseParts, stepResult.raw.stepFunctionCalls);
|
|
149
|
+
next.push({
|
|
150
|
+
role: "user",
|
|
151
|
+
parts: toolResults.map((result) => ({
|
|
152
|
+
functionResponse: {
|
|
153
|
+
// Back to the sanitized wire name the model actually called.
|
|
154
|
+
name: sanitizedNameFor(result.name),
|
|
155
|
+
response: result.error
|
|
156
|
+
? { error: result.error }
|
|
157
|
+
: { result: result.output },
|
|
158
|
+
},
|
|
159
|
+
})),
|
|
160
|
+
});
|
|
161
|
+
return next;
|
|
162
|
+
},
|
|
163
|
+
mapFinishReason(rawStopReason, hadToolCallsAtCap) {
|
|
164
|
+
const mapped = mapGeminiFinishReason(rawStopReason);
|
|
165
|
+
// A turn cut off at the step cap ended because of the cap, not because
|
|
166
|
+
// the last response said "STOP".
|
|
167
|
+
return hadToolCallsAtCap && mapped === "stop" ? "tool-calls" : mapped;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=geminiLoopAdapter.js.map
|
|
@@ -657,10 +657,18 @@ export async function collectStreamChunksIncremental(stream, channel) {
|
|
|
657
657
|
let outputTokens = 0;
|
|
658
658
|
let cacheReadTokens = 0;
|
|
659
659
|
let reasoningTokens = 0;
|
|
660
|
+
// Surfaced so a caller can map SAFETY / MALFORMED_FUNCTION_CALL rather
|
|
661
|
+
// than inferring the turn ended normally. Additive: existing callers that
|
|
662
|
+
// ignore it are unaffected.
|
|
663
|
+
let finishReason;
|
|
660
664
|
for await (const chunk of stream) {
|
|
661
665
|
const chunkRecord = chunk;
|
|
662
666
|
const candidates = chunkRecord.candidates;
|
|
663
667
|
const firstCandidate = candidates?.[0];
|
|
668
|
+
const candidateFinish = firstCandidate?.finishReason;
|
|
669
|
+
if (typeof candidateFinish === "string") {
|
|
670
|
+
finishReason = candidateFinish;
|
|
671
|
+
}
|
|
664
672
|
const chunkContent = firstCandidate?.content;
|
|
665
673
|
if (chunkContent && Array.isArray(chunkContent.parts)) {
|
|
666
674
|
for (const part of chunkContent.parts) {
|
|
@@ -689,6 +697,7 @@ export async function collectStreamChunksIncremental(stream, channel) {
|
|
|
689
697
|
return {
|
|
690
698
|
rawResponseParts,
|
|
691
699
|
stepFunctionCalls,
|
|
700
|
+
finishReason,
|
|
692
701
|
inputTokens,
|
|
693
702
|
outputTokens,
|
|
694
703
|
cacheReadTokens,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type Anthropic from "@anthropic-ai/sdk";
|
|
2
2
|
import type { Tool } from "./tools.js";
|
|
3
|
+
import type { NativeFunctionCall, NativeToolDeclarationsResult } from "./providers.js";
|
|
3
4
|
/**
|
|
4
5
|
* One chunk on the engine's stream.
|
|
5
6
|
*
|
|
@@ -173,6 +174,94 @@ export type AnthropicLoopAdapterConfig = {
|
|
|
173
174
|
noteObservedPromptTokens?: (promptTokens: number) => void;
|
|
174
175
|
abortSignal?: AbortSignal;
|
|
175
176
|
};
|
|
177
|
+
/** What one Gemini step produced, carried to `buildToolResultMessages`. */
|
|
178
|
+
export type GeminiStepRaw = {
|
|
179
|
+
rawResponseParts: unknown[];
|
|
180
|
+
stepFunctionCalls: NativeFunctionCall[];
|
|
181
|
+
};
|
|
182
|
+
/** One turn entry in a Gemini conversation: a role plus its content parts. */
|
|
183
|
+
export type GeminiTurnContent = {
|
|
184
|
+
role: string;
|
|
185
|
+
parts: unknown[];
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Construction input for `createGeminiLoopAdapter`, shared by Google AI Studio
|
|
189
|
+
* and Vertex Gemini. Both issue `models.generateContentStream` and consume the
|
|
190
|
+
* same response shape, so one adapter serves four hand-rolled loops.
|
|
191
|
+
*/
|
|
192
|
+
export type GeminiLoopAdapterCoreConfig = {
|
|
193
|
+
/** Used in log lines and generated tool-call ids. */
|
|
194
|
+
providerLabel: string;
|
|
195
|
+
maxSteps: number;
|
|
196
|
+
/** Build one step's request object (model, contents, config). */
|
|
197
|
+
buildRequest: (conversation: GeminiTurnContent[], step: number) => unknown;
|
|
198
|
+
/** Issue the request. Kept injectable so each provider keeps its own client. */
|
|
199
|
+
sendStep: (request: unknown, signal: AbortSignal) => Promise<AsyncIterable<{
|
|
200
|
+
functionCalls?: NativeFunctionCall[];
|
|
201
|
+
[key: string]: unknown;
|
|
202
|
+
}>>;
|
|
203
|
+
/**
|
|
204
|
+
* The turn's live tool record. Mid-turn `search_tools` discovery hydrates
|
|
205
|
+
* into this, which is what both the declaration refresh and
|
|
206
|
+
* `resolveToolOnMiss` read.
|
|
207
|
+
*/
|
|
208
|
+
liveTools: Record<string, Tool>;
|
|
209
|
+
/**
|
|
210
|
+
* Declarations built for this turn. Carries `originalNameMap`, which keeps
|
|
211
|
+
* Google's function-name sanitization on the adapter side of the engine
|
|
212
|
+
* boundary.
|
|
213
|
+
*/
|
|
214
|
+
declarations?: NativeToolDeclarationsResult;
|
|
215
|
+
toolFailureBreaker?: AgenticLoopToolFailureBreaker;
|
|
216
|
+
/**
|
|
217
|
+
* In-turn context reclaim, run once per step before the request is built.
|
|
218
|
+
* Returns the rebuilt conversation when it reclaimed, undefined when the
|
|
219
|
+
* request still fits.
|
|
220
|
+
*
|
|
221
|
+
* Provider-supplied rather than engine-owned because the two Gemini
|
|
222
|
+
* providers reclaim differently (reclaimAiStudioContext vs
|
|
223
|
+
* reclaimVertexLoopContext) while the engine only decides WHEN to ask. The
|
|
224
|
+
* loops append a model turn plus a tool turn every step with nothing else
|
|
225
|
+
* bounding growth, so a migration that drops this overflows the context
|
|
226
|
+
* window mid-turn and loses every completed step.
|
|
227
|
+
*/
|
|
228
|
+
planReclaim?: (conversation: GeminiTurnContent[], step: number) => GeminiTurnContent[] | undefined;
|
|
229
|
+
/**
|
|
230
|
+
* Usage feedback for the provider's own context guard, called after each
|
|
231
|
+
* step with that step's real token counts.
|
|
232
|
+
*/
|
|
233
|
+
noteUsage?: (inputTokens: number, outputTokens: number) => void;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Opt in to the single MALFORMED_FUNCTION_CALL retry.
|
|
237
|
+
*
|
|
238
|
+
* Vertex Gemini only. AI Studio has no such retry today (confirmed: zero
|
|
239
|
+
* MALFORMED_FUNCTION_CALL handling in its client), and turning it on there
|
|
240
|
+
* would be a behaviour change disguised as a shared refactor. The engine owns
|
|
241
|
+
* the one-retry budget; this only says whether to ask.
|
|
242
|
+
*
|
|
243
|
+
* A union rather than two independent optional fields because the retry is
|
|
244
|
+
* only worth spending a step on if the re-issued request differs from the one
|
|
245
|
+
* that just failed. `runAgenticLoop` falls back to the unchanged conversation
|
|
246
|
+
* when no note builder is supplied (`buildMalformedRetryNote?.(…) ??
|
|
247
|
+
* conversation`), so enabling the retry without one re-sends a byte-identical
|
|
248
|
+
* request and most often reproduces the same malformed call — a step burned
|
|
249
|
+
* for nothing. Requiring the builder here makes that combination unsayable
|
|
250
|
+
* instead of merely discouraged.
|
|
251
|
+
*/
|
|
252
|
+
export type GeminiMalformedRetryConfig = {
|
|
253
|
+
enableMalformedRetry: true;
|
|
254
|
+
/**
|
|
255
|
+
* Append the corrective turn that the retry re-issues with.
|
|
256
|
+
* Provider-supplied because the note is written in the provider's own
|
|
257
|
+
* content shape.
|
|
258
|
+
*/
|
|
259
|
+
buildMalformedRetryNote: (conversation: GeminiTurnContent[]) => GeminiTurnContent[];
|
|
260
|
+
} | {
|
|
261
|
+
enableMalformedRetry?: false;
|
|
262
|
+
buildMalformedRetryNote?: never;
|
|
263
|
+
};
|
|
264
|
+
export type GeminiLoopAdapterConfig = GeminiLoopAdapterCoreConfig & GeminiMalformedRetryConfig;
|
|
176
265
|
export type AgenticLoopOptions = {
|
|
177
266
|
tools?: Record<string, {
|
|
178
267
|
execute?: (args: Record<string, unknown>, opts: unknown) => Promise<unknown>;
|
|
@@ -1738,6 +1738,8 @@ export type NativeFunctionResponse = {
|
|
|
1738
1738
|
export type CollectedChunkResult = {
|
|
1739
1739
|
rawResponseParts: unknown[];
|
|
1740
1740
|
stepFunctionCalls: NativeFunctionCall[];
|
|
1741
|
+
/** Raw `Candidate.finishReason` from the last chunk that carried one. */
|
|
1742
|
+
finishReason?: string;
|
|
1741
1743
|
inputTokens: number;
|
|
1742
1744
|
outputTokens: number;
|
|
1743
1745
|
/**
|
|
@@ -657,10 +657,18 @@ export async function collectStreamChunksIncremental(stream, channel) {
|
|
|
657
657
|
let outputTokens = 0;
|
|
658
658
|
let cacheReadTokens = 0;
|
|
659
659
|
let reasoningTokens = 0;
|
|
660
|
+
// Surfaced so a caller can map SAFETY / MALFORMED_FUNCTION_CALL rather
|
|
661
|
+
// than inferring the turn ended normally. Additive: existing callers that
|
|
662
|
+
// ignore it are unaffected.
|
|
663
|
+
let finishReason;
|
|
660
664
|
for await (const chunk of stream) {
|
|
661
665
|
const chunkRecord = chunk;
|
|
662
666
|
const candidates = chunkRecord.candidates;
|
|
663
667
|
const firstCandidate = candidates?.[0];
|
|
668
|
+
const candidateFinish = firstCandidate?.finishReason;
|
|
669
|
+
if (typeof candidateFinish === "string") {
|
|
670
|
+
finishReason = candidateFinish;
|
|
671
|
+
}
|
|
664
672
|
const chunkContent = firstCandidate?.content;
|
|
665
673
|
if (chunkContent && Array.isArray(chunkContent.parts)) {
|
|
666
674
|
for (const part of chunkContent.parts) {
|
|
@@ -689,6 +697,7 @@ export async function collectStreamChunksIncremental(stream, channel) {
|
|
|
689
697
|
return {
|
|
690
698
|
rawResponseParts,
|
|
691
699
|
stepFunctionCalls,
|
|
700
|
+
finishReason,
|
|
692
701
|
inputTokens,
|
|
693
702
|
outputTokens,
|
|
694
703
|
cacheReadTokens,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type Anthropic from "@anthropic-ai/sdk";
|
|
2
2
|
import type { Tool } from "./tools.js";
|
|
3
|
+
import type { NativeFunctionCall, NativeToolDeclarationsResult } from "./providers.js";
|
|
3
4
|
/**
|
|
4
5
|
* One chunk on the engine's stream.
|
|
5
6
|
*
|
|
@@ -173,6 +174,94 @@ export type AnthropicLoopAdapterConfig = {
|
|
|
173
174
|
noteObservedPromptTokens?: (promptTokens: number) => void;
|
|
174
175
|
abortSignal?: AbortSignal;
|
|
175
176
|
};
|
|
177
|
+
/** What one Gemini step produced, carried to `buildToolResultMessages`. */
|
|
178
|
+
export type GeminiStepRaw = {
|
|
179
|
+
rawResponseParts: unknown[];
|
|
180
|
+
stepFunctionCalls: NativeFunctionCall[];
|
|
181
|
+
};
|
|
182
|
+
/** One turn entry in a Gemini conversation: a role plus its content parts. */
|
|
183
|
+
export type GeminiTurnContent = {
|
|
184
|
+
role: string;
|
|
185
|
+
parts: unknown[];
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Construction input for `createGeminiLoopAdapter`, shared by Google AI Studio
|
|
189
|
+
* and Vertex Gemini. Both issue `models.generateContentStream` and consume the
|
|
190
|
+
* same response shape, so one adapter serves four hand-rolled loops.
|
|
191
|
+
*/
|
|
192
|
+
export type GeminiLoopAdapterCoreConfig = {
|
|
193
|
+
/** Used in log lines and generated tool-call ids. */
|
|
194
|
+
providerLabel: string;
|
|
195
|
+
maxSteps: number;
|
|
196
|
+
/** Build one step's request object (model, contents, config). */
|
|
197
|
+
buildRequest: (conversation: GeminiTurnContent[], step: number) => unknown;
|
|
198
|
+
/** Issue the request. Kept injectable so each provider keeps its own client. */
|
|
199
|
+
sendStep: (request: unknown, signal: AbortSignal) => Promise<AsyncIterable<{
|
|
200
|
+
functionCalls?: NativeFunctionCall[];
|
|
201
|
+
[key: string]: unknown;
|
|
202
|
+
}>>;
|
|
203
|
+
/**
|
|
204
|
+
* The turn's live tool record. Mid-turn `search_tools` discovery hydrates
|
|
205
|
+
* into this, which is what both the declaration refresh and
|
|
206
|
+
* `resolveToolOnMiss` read.
|
|
207
|
+
*/
|
|
208
|
+
liveTools: Record<string, Tool>;
|
|
209
|
+
/**
|
|
210
|
+
* Declarations built for this turn. Carries `originalNameMap`, which keeps
|
|
211
|
+
* Google's function-name sanitization on the adapter side of the engine
|
|
212
|
+
* boundary.
|
|
213
|
+
*/
|
|
214
|
+
declarations?: NativeToolDeclarationsResult;
|
|
215
|
+
toolFailureBreaker?: AgenticLoopToolFailureBreaker;
|
|
216
|
+
/**
|
|
217
|
+
* In-turn context reclaim, run once per step before the request is built.
|
|
218
|
+
* Returns the rebuilt conversation when it reclaimed, undefined when the
|
|
219
|
+
* request still fits.
|
|
220
|
+
*
|
|
221
|
+
* Provider-supplied rather than engine-owned because the two Gemini
|
|
222
|
+
* providers reclaim differently (reclaimAiStudioContext vs
|
|
223
|
+
* reclaimVertexLoopContext) while the engine only decides WHEN to ask. The
|
|
224
|
+
* loops append a model turn plus a tool turn every step with nothing else
|
|
225
|
+
* bounding growth, so a migration that drops this overflows the context
|
|
226
|
+
* window mid-turn and loses every completed step.
|
|
227
|
+
*/
|
|
228
|
+
planReclaim?: (conversation: GeminiTurnContent[], step: number) => GeminiTurnContent[] | undefined;
|
|
229
|
+
/**
|
|
230
|
+
* Usage feedback for the provider's own context guard, called after each
|
|
231
|
+
* step with that step's real token counts.
|
|
232
|
+
*/
|
|
233
|
+
noteUsage?: (inputTokens: number, outputTokens: number) => void;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Opt in to the single MALFORMED_FUNCTION_CALL retry.
|
|
237
|
+
*
|
|
238
|
+
* Vertex Gemini only. AI Studio has no such retry today (confirmed: zero
|
|
239
|
+
* MALFORMED_FUNCTION_CALL handling in its client), and turning it on there
|
|
240
|
+
* would be a behaviour change disguised as a shared refactor. The engine owns
|
|
241
|
+
* the one-retry budget; this only says whether to ask.
|
|
242
|
+
*
|
|
243
|
+
* A union rather than two independent optional fields because the retry is
|
|
244
|
+
* only worth spending a step on if the re-issued request differs from the one
|
|
245
|
+
* that just failed. `runAgenticLoop` falls back to the unchanged conversation
|
|
246
|
+
* when no note builder is supplied (`buildMalformedRetryNote?.(…) ??
|
|
247
|
+
* conversation`), so enabling the retry without one re-sends a byte-identical
|
|
248
|
+
* request and most often reproduces the same malformed call — a step burned
|
|
249
|
+
* for nothing. Requiring the builder here makes that combination unsayable
|
|
250
|
+
* instead of merely discouraged.
|
|
251
|
+
*/
|
|
252
|
+
export type GeminiMalformedRetryConfig = {
|
|
253
|
+
enableMalformedRetry: true;
|
|
254
|
+
/**
|
|
255
|
+
* Append the corrective turn that the retry re-issues with.
|
|
256
|
+
* Provider-supplied because the note is written in the provider's own
|
|
257
|
+
* content shape.
|
|
258
|
+
*/
|
|
259
|
+
buildMalformedRetryNote: (conversation: GeminiTurnContent[]) => GeminiTurnContent[];
|
|
260
|
+
} | {
|
|
261
|
+
enableMalformedRetry?: false;
|
|
262
|
+
buildMalformedRetryNote?: never;
|
|
263
|
+
};
|
|
264
|
+
export type GeminiLoopAdapterConfig = GeminiLoopAdapterCoreConfig & GeminiMalformedRetryConfig;
|
|
176
265
|
export type AgenticLoopOptions = {
|
|
177
266
|
tools?: Record<string, {
|
|
178
267
|
execute?: (args: Record<string, unknown>, opts: unknown) => Promise<unknown>;
|
|
@@ -1738,6 +1738,8 @@ export type NativeFunctionResponse = {
|
|
|
1738
1738
|
export type CollectedChunkResult = {
|
|
1739
1739
|
rawResponseParts: unknown[];
|
|
1740
1740
|
stepFunctionCalls: NativeFunctionCall[];
|
|
1741
|
+
/** Raw `Candidate.finishReason` from the last chunk that carried one. */
|
|
1742
|
+
finishReason?: string;
|
|
1741
1743
|
inputTokens: number;
|
|
1742
1744
|
outputTokens: number;
|
|
1743
1745
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.10.0",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|