@juspay/neurolink 11.8.1 → 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 +3 -3
- package/dist/browser/neurolink.min.js +321 -321
- 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/anthropic/client.js +1 -27
- package/dist/lib/providers/anthropic/loopAdapter.d.ts +26 -0
- package/dist/lib/providers/anthropic/loopAdapter.js +313 -0
- package/dist/lib/providers/anthropic/toolOutput.d.ts +9 -0
- package/dist/lib/providers/anthropic/toolOutput.js +39 -0
- package/dist/lib/providers/googleNativeGemini3/utils.js +9 -0
- package/dist/lib/types/loopEngine.d.ts +141 -0
- package/dist/lib/types/providers.d.ts +8 -0
- package/dist/providers/anthropic/client.js +1 -27
- package/dist/providers/anthropic/loopAdapter.d.ts +26 -0
- package/dist/providers/anthropic/loopAdapter.js +312 -0
- package/dist/providers/anthropic/toolOutput.d.ts +9 -0
- package/dist/providers/anthropic/toolOutput.js +38 -0
- package/dist/providers/googleNativeGemini3/utils.js +9 -0
- package/dist/types/loopEngine.d.ts +141 -0
- package/dist/types/providers.d.ts +8 -0
- package/package.json +4 -2
|
@@ -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
|
|
@@ -22,6 +22,7 @@ import { redactUrlCredentials } from "../../utils/logSanitize.js";
|
|
|
22
22
|
import { ANTHROPIC_MAX_CACHE_BREAKPOINTS, applyAnthropicHistoryCacheBreakpoints, countAnthropicCacheMarkers, } from "../../utils/anthropicCacheBreakpoints.js";
|
|
23
23
|
import { calculateCost } from "../../utils/pricing.js";
|
|
24
24
|
import { resolveDeferredTool } from "../../tools/toolDiscovery.js";
|
|
25
|
+
import { stringifyAnthropicToolOutput } from "./toolOutput.js";
|
|
25
26
|
import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../../utils/providerConfig.js";
|
|
26
27
|
import { composeAbortSignals, createTimeoutController, mergeAbortSignals, } from "../../utils/timeout.js";
|
|
27
28
|
import { resolveToolChoice } from "../../utils/toolChoice.js";
|
|
@@ -183,33 +184,6 @@ const detectAuthMethod = (oauthToken) => {
|
|
|
183
184
|
// ───────────────────────────────────────────────────────────────────────────
|
|
184
185
|
// Native Messages-API conversion helpers (NeuroLink/V3 shapes → Anthropic)
|
|
185
186
|
// ───────────────────────────────────────────────────────────────────────────
|
|
186
|
-
/** Serialize a tool-result `output` into text for a tool_result block. */
|
|
187
|
-
const stringifyAnthropicToolOutput = (output) => {
|
|
188
|
-
if (output === null || output === undefined) {
|
|
189
|
-
return "";
|
|
190
|
-
}
|
|
191
|
-
if (typeof output === "string") {
|
|
192
|
-
return output;
|
|
193
|
-
}
|
|
194
|
-
const o = output;
|
|
195
|
-
if (o.type === "text" && typeof o.value === "string") {
|
|
196
|
-
return o.value;
|
|
197
|
-
}
|
|
198
|
-
if (o.type === "json" || o.type === "error-json") {
|
|
199
|
-
try {
|
|
200
|
-
return JSON.stringify(o.value);
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
return String(o.value);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
try {
|
|
207
|
-
return JSON.stringify(output);
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
return String(output);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
187
|
/**
|
|
214
188
|
* Convert NeuroLink/V3-shaped messages (the shape produced by
|
|
215
189
|
* buildMessagesForStream and by the AI-SDK prompt on the V3 doGenerate path)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct Anthropic's adapter onto the shared agentic loop engine.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is the wire-format half of the turn: building one Messages
|
|
5
|
+
* request, folding an SSE event sequence into content blocks, serializing tool
|
|
6
|
+
* results back into the conversation, and mapping the stop reason. The turn
|
|
7
|
+
* loop, the step cap, tool dispatch, retry and usage accumulation belong to
|
|
8
|
+
* `runAgenticLoop`.
|
|
9
|
+
*
|
|
10
|
+
* Two behaviours here are easy to lose in a migration and are called out
|
|
11
|
+
* because losing either is silent:
|
|
12
|
+
*
|
|
13
|
+
* - Thinking deltas ride the channel as `{ content: "", reasoning }`. The
|
|
14
|
+
* engine's chunk type carries `reasoning` for exactly this reason; a
|
|
15
|
+
* channel that only understood `content` would drop every thinking delta
|
|
16
|
+
* while the text path kept working.
|
|
17
|
+
* - `resolveToolOnMiss` is wired to real deferred-catalog hydration, not as
|
|
18
|
+
* interface decoration. The pre-migration loop resolves every tool call as
|
|
19
|
+
* `toolsRecord[name] ?? resolveDeferredTool(toolsRecord, name)`, which is
|
|
20
|
+
* how a cataloged tool the model calls without having loaded it via
|
|
21
|
+
* `search_tools` gets found. Dropping it would break those tools with a
|
|
22
|
+
* "Tool not found" that looks like a hallucination.
|
|
23
|
+
*/
|
|
24
|
+
import type Anthropic from "@anthropic-ai/sdk";
|
|
25
|
+
import type { AgenticLoopAdapter, AnthropicLoopAdapterConfig } from "../../types/index.js";
|
|
26
|
+
export declare function createAnthropicLoopAdapter(config: AnthropicLoopAdapterConfig): AgenticLoopAdapter<Anthropic.Messages.MessageParam[], Anthropic.Messages.ContentBlockParam[]>;
|