@juspay/neurolink 11.8.0 → 11.9.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 +321 -321
- package/dist/core/loopEngine.d.ts +2 -4
- package/dist/lib/core/loopEngine.d.ts +2 -4
- 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/types/loopEngine.d.ts +67 -3
- package/dist/lib/types/providers.d.ts +6 -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/types/loopEngine.d.ts +67 -3
- package/dist/types/providers.d.ts +6 -0
- package/package.json +4 -2
|
@@ -0,0 +1,312 @@
|
|
|
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 { resolveDeferredTool } from "../../tools/toolDiscovery.js";
|
|
25
|
+
import { stringifyAnthropicToolOutput } from "./toolOutput.js";
|
|
26
|
+
/** Map Anthropic's stop_reason onto the unified finish reason. */
|
|
27
|
+
function mapAnthropicFinishReason(rawStopReason, hadToolCallsAtCap) {
|
|
28
|
+
switch (rawStopReason) {
|
|
29
|
+
case "max_tokens":
|
|
30
|
+
return "length";
|
|
31
|
+
case "tool_use":
|
|
32
|
+
return "tool-calls";
|
|
33
|
+
case "refusal":
|
|
34
|
+
return "content-filter";
|
|
35
|
+
default:
|
|
36
|
+
return hadToolCallsAtCap ? "tool-calls" : "stop";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function createAnthropicLoopAdapter(config) {
|
|
40
|
+
return {
|
|
41
|
+
providerLabel: "anthropic",
|
|
42
|
+
maxSteps: config.maxSteps,
|
|
43
|
+
...(config.toolFailureBreaker
|
|
44
|
+
? { toolFailureBreaker: config.toolFailureBreaker }
|
|
45
|
+
: {}),
|
|
46
|
+
/**
|
|
47
|
+
* The engine decides WHEN to reclaim; the provider decides HOW, in its own
|
|
48
|
+
* concrete message types. Not optional dressing: this loop appends an
|
|
49
|
+
* assistant tool_use message and a user tool_result message every step,
|
|
50
|
+
* and nothing else bounds that growth.
|
|
51
|
+
*/
|
|
52
|
+
...(config.planReclaim
|
|
53
|
+
? {
|
|
54
|
+
planReclaim: (conversation, step) => {
|
|
55
|
+
const reclaimed = config.planReclaim?.(conversation, step);
|
|
56
|
+
return reclaimed ? { conversation: reclaimed } : undefined;
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
resolveToolOnMiss: (name) => {
|
|
61
|
+
const hydrated = resolveDeferredTool(config.toolsRecord, name);
|
|
62
|
+
const execute = hydrated?.execute;
|
|
63
|
+
if (!execute) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
// Wrapped rather than handed over directly: the engine's hook types
|
|
67
|
+
// `opts` as `unknown`, and a function declaring a narrower options type
|
|
68
|
+
// is not assignable to one accepting `unknown`. One assertion at the
|
|
69
|
+
// boundary, never a double assertion through `unknown`.
|
|
70
|
+
return {
|
|
71
|
+
execute: async (args, opts) => execute(args, opts),
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
buildStepRequest(conversation, step) {
|
|
75
|
+
return { raw: config.buildParams(conversation, step) };
|
|
76
|
+
},
|
|
77
|
+
async executeStep(request, channel, signal) {
|
|
78
|
+
const params = request.raw;
|
|
79
|
+
// Single assertion, not a double: `messages.create` returns a union of
|
|
80
|
+
// Message and Stream, and Stream<RawMessageStreamEvent> already IS an
|
|
81
|
+
// AsyncIterable of that event, so the two types overlap and the
|
|
82
|
+
// compiler still checks the narrowing.
|
|
83
|
+
const events = (await config.client.messages.create({ ...params, stream: true },
|
|
84
|
+
// The engine's signal, not config's: runAgenticLoop already derives
|
|
85
|
+
// it from the caller's abortSignal, so preferring config's would
|
|
86
|
+
// ignore engine-initiated cancellation entirely.
|
|
87
|
+
{ signal }));
|
|
88
|
+
const textByIndex = new Map();
|
|
89
|
+
const toolByIndex = new Map();
|
|
90
|
+
const thinkingByIndex = new Map();
|
|
91
|
+
const redactedByIndex = new Map();
|
|
92
|
+
let text = "";
|
|
93
|
+
let reasoning = "";
|
|
94
|
+
let rawStopReason;
|
|
95
|
+
let inputTokens = 0;
|
|
96
|
+
let outputTokens = 0;
|
|
97
|
+
let cacheReadTokens = 0;
|
|
98
|
+
let cacheWriteTokens = 0;
|
|
99
|
+
let stepOutputTokens = 0;
|
|
100
|
+
for await (const rawEvent of events) {
|
|
101
|
+
if (signal.aborted) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
// Narrowed through the SDK's own discriminated union rather than
|
|
105
|
+
// re-declared: `RawMessageStreamEvent` already describes every event
|
|
106
|
+
// shape, so `event.type` checks below are compiler-checked instead of
|
|
107
|
+
// asserted.
|
|
108
|
+
const event = rawEvent;
|
|
109
|
+
if (event.type === "message_start") {
|
|
110
|
+
const usage = event.message?.usage;
|
|
111
|
+
inputTokens += usage?.input_tokens ?? 0;
|
|
112
|
+
const startOutput = usage?.output_tokens ?? 0;
|
|
113
|
+
outputTokens += startOutput - stepOutputTokens;
|
|
114
|
+
stepOutputTokens = startOutput;
|
|
115
|
+
// Anthropic reports cache reads/writes separately from input_tokens
|
|
116
|
+
// on this same event; without these the stream drops all cache
|
|
117
|
+
// accounting.
|
|
118
|
+
cacheReadTokens += usage?.cache_read_input_tokens ?? 0;
|
|
119
|
+
cacheWriteTokens += usage?.cache_creation_input_tokens ?? 0;
|
|
120
|
+
// The guard calibrates from the FULL prompt size, not input_tokens
|
|
121
|
+
// alone: on a cache hit the uncached remainder is tiny and using it
|
|
122
|
+
// would let the guard drift far under the real cost.
|
|
123
|
+
config.noteObservedPromptTokens?.((usage?.input_tokens ?? 0) +
|
|
124
|
+
(usage?.cache_read_input_tokens ?? 0) +
|
|
125
|
+
(usage?.cache_creation_input_tokens ?? 0));
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (event.type === "content_block_start") {
|
|
129
|
+
const index = event.index ?? 0;
|
|
130
|
+
// A redacted_thinking block carries its whole payload here and
|
|
131
|
+
// produces no deltas, so if it is not captured on this event it is
|
|
132
|
+
// never seen again. Anthropic validates the thinking chain when
|
|
133
|
+
// extended thinking continues across a tool-use turn, so a missing
|
|
134
|
+
// one fails the NEXT request — and only for accounts where safety
|
|
135
|
+
// redaction actually triggers, which is why it survives testing.
|
|
136
|
+
if (event.content_block?.type === "redacted_thinking") {
|
|
137
|
+
const data = event.content_block.data;
|
|
138
|
+
if (typeof data === "string") {
|
|
139
|
+
redactedByIndex.set(index, data);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (event.content_block?.type === "tool_use") {
|
|
143
|
+
toolByIndex.set(index, {
|
|
144
|
+
id: event.content_block.id ?? "",
|
|
145
|
+
name: event.content_block.name ?? "",
|
|
146
|
+
inputJson: "",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (event.type === "content_block_delta") {
|
|
152
|
+
const index = event.index ?? 0;
|
|
153
|
+
const delta = event.delta;
|
|
154
|
+
if (delta?.type === "text_delta" && delta.text) {
|
|
155
|
+
text += delta.text;
|
|
156
|
+
textByIndex.set(index, (textByIndex.get(index) ?? "") + delta.text);
|
|
157
|
+
channel.push({ content: delta.text });
|
|
158
|
+
}
|
|
159
|
+
else if (delta?.type === "thinking_delta" && delta.thinking) {
|
|
160
|
+
const acc = thinkingByIndex.get(index) ?? {
|
|
161
|
+
text: "",
|
|
162
|
+
signature: "",
|
|
163
|
+
};
|
|
164
|
+
acc.text += delta.thinking;
|
|
165
|
+
thinkingByIndex.set(index, acc);
|
|
166
|
+
reasoning += delta.thinking;
|
|
167
|
+
// Reasoning rides its own field; `content` stays a present string
|
|
168
|
+
// so plain-text consumers are unaffected.
|
|
169
|
+
channel.push({ content: "", reasoning: delta.thinking });
|
|
170
|
+
}
|
|
171
|
+
else if (delta?.type === "signature_delta" && delta.signature) {
|
|
172
|
+
const acc = thinkingByIndex.get(index) ?? {
|
|
173
|
+
text: "",
|
|
174
|
+
signature: "",
|
|
175
|
+
};
|
|
176
|
+
acc.signature += delta.signature;
|
|
177
|
+
thinkingByIndex.set(index, acc);
|
|
178
|
+
}
|
|
179
|
+
else if (delta?.type === "input_json_delta" && delta.partial_json) {
|
|
180
|
+
const pending = toolByIndex.get(index);
|
|
181
|
+
if (pending) {
|
|
182
|
+
pending.inputJson += delta.partial_json;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (event.type === "message_delta") {
|
|
188
|
+
rawStopReason = event.delta?.stop_reason ?? rawStopReason;
|
|
189
|
+
const cumulative = event.usage?.output_tokens ?? stepOutputTokens;
|
|
190
|
+
outputTokens += cumulative - stepOutputTokens;
|
|
191
|
+
stepOutputTokens = cumulative;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// JSON.parse happily yields null, an array or a primitive, and an
|
|
195
|
+
// assertion converts none of them. Both `tool_use.input` and
|
|
196
|
+
// AgenticLoopToolCall.args require an object, so anything else becomes
|
|
197
|
+
// {} rather than being passed through as invalid wire content.
|
|
198
|
+
const parseArgs = (json) => {
|
|
199
|
+
if (!json) {
|
|
200
|
+
return {};
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const parsed = JSON.parse(json);
|
|
204
|
+
return typeof parsed === "object" &&
|
|
205
|
+
parsed !== null &&
|
|
206
|
+
!Array.isArray(parsed)
|
|
207
|
+
? parsed
|
|
208
|
+
: {};
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
// Rebuild the assistant turn in wire order so it can be replayed as the
|
|
215
|
+
// assistant message on the next step. Text that arrived before a tool
|
|
216
|
+
// call has to survive here, or the model stops seeing its own reasoning
|
|
217
|
+
// mid-turn.
|
|
218
|
+
const blocks = [];
|
|
219
|
+
const indices = new Set([
|
|
220
|
+
...textByIndex.keys(),
|
|
221
|
+
...toolByIndex.keys(),
|
|
222
|
+
...thinkingByIndex.keys(),
|
|
223
|
+
...redactedByIndex.keys(),
|
|
224
|
+
]);
|
|
225
|
+
for (const index of [...indices].sort((a, b) => a - b)) {
|
|
226
|
+
// Thinking blocks are replayed with their signature. Anthropic
|
|
227
|
+
// validates that signature when extended thinking continues across
|
|
228
|
+
// turns, so dropping the block — or keeping the text without the
|
|
229
|
+
// signature — breaks the next step of a thinking turn.
|
|
230
|
+
const redacted = redactedByIndex.get(index);
|
|
231
|
+
if (redacted) {
|
|
232
|
+
blocks.push({ type: "redacted_thinking", data: redacted });
|
|
233
|
+
}
|
|
234
|
+
const thinking = thinkingByIndex.get(index);
|
|
235
|
+
// Both halves required: Anthropic's thinking block carries a
|
|
236
|
+
// mandatory signature, and replaying one with an empty string is
|
|
237
|
+
// rejected outright. A thinking block that never received a
|
|
238
|
+
// signature_delta is dropped rather than sent unsigned.
|
|
239
|
+
if (thinking?.text && thinking.signature) {
|
|
240
|
+
blocks.push({
|
|
241
|
+
type: "thinking",
|
|
242
|
+
thinking: thinking.text,
|
|
243
|
+
signature: thinking.signature,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const blockText = textByIndex.get(index);
|
|
247
|
+
if (blockText) {
|
|
248
|
+
blocks.push({
|
|
249
|
+
type: "text",
|
|
250
|
+
text: blockText,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const pending = toolByIndex.get(index);
|
|
254
|
+
if (pending) {
|
|
255
|
+
blocks.push({
|
|
256
|
+
type: "tool_use",
|
|
257
|
+
id: pending.id,
|
|
258
|
+
name: pending.name,
|
|
259
|
+
input: parseArgs(pending.inputJson),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const allCalls = [...toolByIndex.values()].map((pending) => ({
|
|
264
|
+
id: pending.id,
|
|
265
|
+
name: pending.name,
|
|
266
|
+
args: parseArgs(pending.inputJson),
|
|
267
|
+
}));
|
|
268
|
+
// A terminal structured-output call ends the turn: its arguments are the
|
|
269
|
+
// answer. Reporting it as text and leaving it out of `toolCalls` is what
|
|
270
|
+
// routes it through the engine's ordinary zero-tool-calls exit, so it is
|
|
271
|
+
// never dispatched, never counted against the breaker, and never shows
|
|
272
|
+
// up as a tool execution.
|
|
273
|
+
const terminal = config.finalResultToolName
|
|
274
|
+
? allCalls.find((call) => call.name === config.finalResultToolName)
|
|
275
|
+
: undefined;
|
|
276
|
+
const toolCalls = terminal ? [] : allCalls;
|
|
277
|
+
const finalText = terminal ? JSON.stringify(terminal.args) : text;
|
|
278
|
+
return {
|
|
279
|
+
text: finalText,
|
|
280
|
+
...(reasoning ? { reasoning } : {}),
|
|
281
|
+
toolCalls,
|
|
282
|
+
usage: {
|
|
283
|
+
inputTokens,
|
|
284
|
+
outputTokens,
|
|
285
|
+
cacheReadTokens,
|
|
286
|
+
cacheWriteTokens,
|
|
287
|
+
},
|
|
288
|
+
rawStopReason,
|
|
289
|
+
raw: blocks,
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
buildToolResultMessages(conversation, stepResult, toolResults) {
|
|
293
|
+
const assistantMessage = {
|
|
294
|
+
role: "assistant",
|
|
295
|
+
content: stepResult.raw,
|
|
296
|
+
};
|
|
297
|
+
const resultMessage = {
|
|
298
|
+
role: "user",
|
|
299
|
+
content: toolResults.map((result) => ({
|
|
300
|
+
type: "tool_result",
|
|
301
|
+
tool_use_id: result.id,
|
|
302
|
+
content: result.error
|
|
303
|
+
? `Error executing tool ${result.name}: ${result.error}`
|
|
304
|
+
: stringifyAnthropicToolOutput(result.output),
|
|
305
|
+
...(result.error ? { is_error: true } : {}),
|
|
306
|
+
})),
|
|
307
|
+
};
|
|
308
|
+
return [...conversation, assistantMessage, resultMessage];
|
|
309
|
+
},
|
|
310
|
+
mapFinishReason: mapAnthropicFinishReason,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-result serialization shared by Anthropic's client and its loop adapter.
|
|
3
|
+
*
|
|
4
|
+
* Extracted rather than exported from `client.ts` because the adapter is
|
|
5
|
+
* imported BY the client: importing back the other way would close a cycle.
|
|
6
|
+
* The behaviour is unchanged from the module-private version it replaces.
|
|
7
|
+
*/
|
|
8
|
+
/** Serialize a tool-result `output` into text for a tool_result block. */
|
|
9
|
+
export declare const stringifyAnthropicToolOutput: (output: unknown) => string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-result serialization shared by Anthropic's client and its loop adapter.
|
|
3
|
+
*
|
|
4
|
+
* Extracted rather than exported from `client.ts` because the adapter is
|
|
5
|
+
* imported BY the client: importing back the other way would close a cycle.
|
|
6
|
+
* The behaviour is unchanged from the module-private version it replaces.
|
|
7
|
+
*/
|
|
8
|
+
/** Serialize a tool-result `output` into text for a tool_result block. */
|
|
9
|
+
export const stringifyAnthropicToolOutput = (output) => {
|
|
10
|
+
if (output === null || output === undefined) {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
if (typeof output === "string") {
|
|
14
|
+
return output;
|
|
15
|
+
}
|
|
16
|
+
const o = output;
|
|
17
|
+
if (o.type === "text" && typeof o.value === "string") {
|
|
18
|
+
return o.value;
|
|
19
|
+
}
|
|
20
|
+
if (o.type === "json" || o.type === "error-json") {
|
|
21
|
+
try {
|
|
22
|
+
// `?? String(...)`: JSON.stringify returns undefined for an undefined
|
|
23
|
+
// value, which would break this function's string contract and emit an
|
|
24
|
+
// invalid tool_result.content.
|
|
25
|
+
return JSON.stringify(o.value) ?? String(o.value);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return String(o.value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
// Same guard: a function or symbol stringifies to undefined.
|
|
33
|
+
return JSON.stringify(output) ?? String(output);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return String(output);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
import type Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import type { Tool } from "./tools.js";
|
|
3
|
+
/**
|
|
4
|
+
* One chunk on the engine's stream.
|
|
5
|
+
*
|
|
6
|
+
* `reasoning` is carried alongside `content` rather than instead of it: the
|
|
7
|
+
* providers that emit extended thinking (direct Anthropic, Google AI Studio,
|
|
8
|
+
* Vertex) push a chunk with empty `content` and the thinking delta in
|
|
9
|
+
* `reasoning`, so a channel typed `{ content: string }` alone would drop
|
|
10
|
+
* every thinking delta the moment those providers move onto the engine —
|
|
11
|
+
* silently, since the text path would keep working.
|
|
12
|
+
*/
|
|
13
|
+
export type AgenticLoopChunk = {
|
|
14
|
+
content: string;
|
|
15
|
+
reasoning?: string;
|
|
16
|
+
};
|
|
1
17
|
export type AgenticLoopToolCall = {
|
|
2
18
|
id: string;
|
|
3
19
|
name: string;
|
|
@@ -97,9 +113,7 @@ export type AgenticLoopAdapter<TConversation = unknown, TRaw = unknown> = {
|
|
|
97
113
|
} | undefined;
|
|
98
114
|
buildStepRequest(conversation: TConversation, step: number): AgenticLoopStepRequest;
|
|
99
115
|
executeStep(request: AgenticLoopStepRequest, channel: {
|
|
100
|
-
push(chunk:
|
|
101
|
-
content: string;
|
|
102
|
-
}): void;
|
|
116
|
+
push(chunk: AgenticLoopChunk): void;
|
|
103
117
|
}, signal: AbortSignal): Promise<AgenticLoopStepResult<TRaw>>;
|
|
104
118
|
buildToolResultMessages(conversation: TConversation, stepResult: AgenticLoopStepResult<TRaw>, toolResults: AgenticLoopToolCallResult[]): TConversation;
|
|
105
119
|
mapFinishReason(rawStopReason: string | undefined, hadToolCalls: boolean): string;
|
|
@@ -109,6 +123,56 @@ export type AgenticLoopAdapter<TConversation = unknown, TRaw = unknown> = {
|
|
|
109
123
|
isMalformedStep?(stepResult: AgenticLoopStepResult<TRaw>): boolean;
|
|
110
124
|
buildMalformedRetryNote?(conversation: TConversation): TConversation;
|
|
111
125
|
};
|
|
126
|
+
/**
|
|
127
|
+
* Construction input for `createAnthropicLoopAdapter`, shared by direct
|
|
128
|
+
* Anthropic and Vertex+Claude.
|
|
129
|
+
*
|
|
130
|
+
* `toolFailureBreaker` is the one field the two call sites must differ on:
|
|
131
|
+
* Vertex+Claude ports the Gemini loops' failure-strike breaker, native
|
|
132
|
+
* Anthropic has never had one, and setting it for both would change native
|
|
133
|
+
* Anthropic's behaviour under the guise of a shared refactor.
|
|
134
|
+
*/
|
|
135
|
+
export type AnthropicLoopAdapterConfig = {
|
|
136
|
+
client: Pick<Anthropic, "messages">;
|
|
137
|
+
maxSteps: number;
|
|
138
|
+
/**
|
|
139
|
+
* Build one step's request. A closure so the per-turn work the caller
|
|
140
|
+
* already does — system prompt, tool declarations, sampling, thinking
|
|
141
|
+
* config, cache breakpoints — stays where it is rather than moving here.
|
|
142
|
+
*/
|
|
143
|
+
buildParams: (conversation: Anthropic.Messages.MessageParam[], step: number) => Anthropic.Messages.MessageCreateParams;
|
|
144
|
+
/** The turn's live tool record, used for deferred-catalog resolution. */
|
|
145
|
+
toolsRecord: Record<string, Tool>;
|
|
146
|
+
/**
|
|
147
|
+
* Name of the terminal structured-output tool when one is in play. A call
|
|
148
|
+
* to it ends the turn: its arguments ARE the answer, so it is reported as
|
|
149
|
+
* text and omitted from `toolCalls`, which routes it through the engine's
|
|
150
|
+
* ordinary zero-tool-calls exit.
|
|
151
|
+
*/
|
|
152
|
+
finalResultToolName?: string;
|
|
153
|
+
toolFailureBreaker?: AgenticLoopToolFailureBreaker;
|
|
154
|
+
/**
|
|
155
|
+
* In-turn context reclaim, run once per step before the request is built.
|
|
156
|
+
* Returns the rebuilt conversation when it reclaimed, undefined while the
|
|
157
|
+
* request still fits — leaving history byte-identical in the common case so
|
|
158
|
+
* the rolling prompt-cache prefix stays valid.
|
|
159
|
+
*
|
|
160
|
+
* Provider-supplied because the guard decides and the caller mutates in its
|
|
161
|
+
* own concrete types: dropping an assistant tool_use message together with
|
|
162
|
+
* its paired user tool_result is what keeps blocks paired. The loop appends
|
|
163
|
+
* both every step with nothing else bounding growth, so a migration that
|
|
164
|
+
* drops this overflows the window mid-turn.
|
|
165
|
+
*/
|
|
166
|
+
planReclaim?: (conversation: Anthropic.Messages.MessageParam[], step: number) => Anthropic.Messages.MessageParam[] | undefined;
|
|
167
|
+
/**
|
|
168
|
+
* Calibration feedback for the provider's reclaim guard: the FULL prompt
|
|
169
|
+
* size for the step just made — uncached input plus both cache tiers.
|
|
170
|
+
* Passing input_tokens alone reads a cache-hit step as tiny and lets the
|
|
171
|
+
* guard drift far under the real cost.
|
|
172
|
+
*/
|
|
173
|
+
noteObservedPromptTokens?: (promptTokens: number) => void;
|
|
174
|
+
abortSignal?: AbortSignal;
|
|
175
|
+
};
|
|
112
176
|
export type AgenticLoopOptions = {
|
|
113
177
|
tools?: Record<string, {
|
|
114
178
|
execute?: (args: Record<string, unknown>, opts: unknown) => Promise<unknown>;
|
|
@@ -842,6 +842,12 @@ export type BedrockContentBlock = {
|
|
|
842
842
|
export type BedrockPendingContentBlock = BedrockContentBlock & {
|
|
843
843
|
_inputBuffer?: string;
|
|
844
844
|
};
|
|
845
|
+
/** A tool_use block being assembled across Anthropic `input_json_delta` events. */
|
|
846
|
+
export type AnthropicPendingToolUse = {
|
|
847
|
+
id: string;
|
|
848
|
+
name: string;
|
|
849
|
+
inputJson: string;
|
|
850
|
+
};
|
|
845
851
|
/**
|
|
846
852
|
* Bedrock message structure
|
|
847
853
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.9.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": {
|
|
@@ -213,7 +213,9 @@
|
|
|
213
213
|
"test:vector-pgvector": "npx tsx test/continuous-test-suite-vector-pgvector.ts",
|
|
214
214
|
"test:vector-pinecone": "npx tsx test/continuous-test-suite-vector-pinecone.ts",
|
|
215
215
|
"test:bedrock-loop-characterization": "tsx test/continuous-test-suite-bedrock-loop-characterization.ts",
|
|
216
|
-
"test:sagemaker-streaming": "tsx test/continuous-test-suite-sagemaker-streaming.ts"
|
|
216
|
+
"test:sagemaker-streaming": "tsx test/continuous-test-suite-sagemaker-streaming.ts",
|
|
217
|
+
"test:anthropic-loop-characterization": "tsx test/continuous-test-suite-anthropic-loop-characterization.ts",
|
|
218
|
+
"test:aistudio-loop-characterization": "tsx test/continuous-test-suite-aistudio-loop-characterization.ts"
|
|
217
219
|
},
|
|
218
220
|
"files": [
|
|
219
221
|
"dist",
|