@juspay/neurolink 11.11.0 → 11.11.2
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 +1 -9
- package/dist/browser/neurolink.min.js +392 -392
- package/dist/lib/providers/anthropic/client.js +177 -295
- package/dist/lib/providers/anthropic/loopAdapter.js +3 -0
- package/dist/lib/providers/googleAiStudio/client.js +10 -1
- package/dist/lib/types/loopEngine.d.ts +12 -0
- package/dist/providers/anthropic/client.js +177 -295
- package/dist/providers/anthropic/loopAdapter.js +3 -0
- package/dist/providers/googleAiStudio/client.js +10 -1
- package/dist/types/loopEngine.d.ts +12 -0
- package/package.json +1 -1
|
@@ -21,8 +21,9 @@ import { estimateTokens } from "../../utils/tokenEstimation.js";
|
|
|
21
21
|
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
|
-
import { resolveDeferredTool } from "../../tools/toolDiscovery.js";
|
|
25
24
|
import { stringifyAnthropicToolOutput } from "./toolOutput.js";
|
|
25
|
+
import { createAnthropicLoopAdapter } from "./loopAdapter.js";
|
|
26
|
+
import { runAgenticLoop } from "../../core/loopEngine.js";
|
|
26
27
|
import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../../utils/providerConfig.js";
|
|
27
28
|
import { composeAbortSignals, createTimeoutController, mergeAbortSignals, } from "../../utils/timeout.js";
|
|
28
29
|
import { resolveToolChoice } from "../../utils/toolChoice.js";
|
|
@@ -31,7 +32,6 @@ import { NoOutputGeneratedError } from "../../utils/generationErrors.js";
|
|
|
31
32
|
import { buildNoOutputSentinel, stampNoOutputSpan, } from "../../utils/noOutputSentinel.js";
|
|
32
33
|
import { convertZodToJsonSchema } from "../../utils/schemaConversion.js";
|
|
33
34
|
import { resolveClaudeMaxTokens } from "../../utils/tokenLimits.js";
|
|
34
|
-
import { withProviderRetry } from "../../utils/providerRetry.js";
|
|
35
35
|
import { toAnthropicImageBlock, fileToAnthropicBlock, } from "../anthropicImageBlocks.js";
|
|
36
36
|
import { resolveSamplingParams } from "../../models/modelRegistry.js";
|
|
37
37
|
import { createDeferredAnalytics, stringifyToolInput, } from "../openaiChatCompletionsClient.js";
|
|
@@ -39,7 +39,7 @@ import { createStreamChannel } from "../../core/streamChannel.js";
|
|
|
39
39
|
import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
|
|
40
40
|
import { ANTHROPIC_BETA_HEADERS } from "./constants.js";
|
|
41
41
|
import { cacheControlOf } from "./cacheControl.js";
|
|
42
|
-
import { appendFinalResultInstruction, appendFinalResultTool, FINAL_RESULT_TOOL_NAME,
|
|
42
|
+
import { appendFinalResultInstruction, appendFinalResultTool, FINAL_RESULT_TOOL_NAME, } from "./structuredOutput.js";
|
|
43
43
|
// AnthropicProviderConfig is imported from types/providers.ts
|
|
44
44
|
// Re-export for backward compatibility
|
|
45
45
|
// Configuration helpers - now using consolidated utility
|
|
@@ -1495,32 +1495,21 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1495
1495
|
estimateTokens(text(tools), "anthropic"));
|
|
1496
1496
|
};
|
|
1497
1497
|
const runLoop = async () => {
|
|
1498
|
-
const conversation = payload.messages.slice();
|
|
1499
1498
|
// The provider's REAL prompt-token count for the previous step,
|
|
1500
1499
|
// calibrating the guard's char-based estimate for free, paired with the
|
|
1501
1500
|
// guard's own estimate for that same request — a ratio between counts of
|
|
1502
1501
|
// two different payloads would be meaningless.
|
|
1503
1502
|
let lastObservedPromptTokens;
|
|
1504
1503
|
let lastSentEstimate;
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
anthropicTools.push(...(toNativeToolDeclarations(hydrated, "input_schema") ?? []));
|
|
1515
|
-
logger.info(`[Anthropic] ${Object.keys(hydrated).length} tool(s) hydrated mid-turn via discovery: ${Object.keys(hydrated).join(", ")}`);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
// In-turn context guard. This loop appends an assistant tool_use
|
|
1519
|
-
// message plus a user tool_result message every step — growth the
|
|
1520
|
-
// pre-dispatch budget check never sees. Without it a long agentic run
|
|
1521
|
-
// overflows the window mid-loop and loses every completed step.
|
|
1522
|
-
// Returns undefined while the request still fits, leaving the history
|
|
1523
|
-
// byte-identical so the rolling cache prefix below stays valid.
|
|
1504
|
+
/**
|
|
1505
|
+
* Reclaim, as a PURE function of the conversation.
|
|
1506
|
+
*
|
|
1507
|
+
* The hand-rolled loop this replaces rebuilt in place
|
|
1508
|
+
* (`conversation.length = 0; conversation.push(...rebuilt)`) because it
|
|
1509
|
+
* owned the array. The engine owns it now and assigns what this
|
|
1510
|
+
* returns, so mutating here would corrupt a retried or reclaimed step.
|
|
1511
|
+
*/
|
|
1512
|
+
const planReclaim = (conversation) => {
|
|
1524
1513
|
const reclaim = planAnthropicLoopReclaim({
|
|
1525
1514
|
conversation,
|
|
1526
1515
|
availableInputTokens: getAvailableInputTokens("anthropic", modelId, options.maxTokens ?? undefined),
|
|
@@ -1535,69 +1524,78 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1535
1524
|
lastSentEstimate = tokens;
|
|
1536
1525
|
},
|
|
1537
1526
|
});
|
|
1538
|
-
if (reclaim) {
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
const message = conversation[i];
|
|
1550
|
-
if (truncateSet.has(i) && Array.isArray(message.content)) {
|
|
1551
|
-
rebuilt.push({
|
|
1552
|
-
...message,
|
|
1553
|
-
content: message.content.map((block) => block.type === "tool_result"
|
|
1554
|
-
? {
|
|
1555
|
-
...block,
|
|
1556
|
-
content: previewAnthropicToolResultText(typeof block.content === "string"
|
|
1557
|
-
? block.content
|
|
1558
|
-
: (JSON.stringify(block.content) ?? "")),
|
|
1559
|
-
}
|
|
1560
|
-
: block),
|
|
1561
|
-
});
|
|
1562
|
-
continue;
|
|
1563
|
-
}
|
|
1564
|
-
rebuilt.push(message);
|
|
1527
|
+
if (!reclaim) {
|
|
1528
|
+
return undefined;
|
|
1529
|
+
}
|
|
1530
|
+
// Dropping an assistant tool_use message together with its user
|
|
1531
|
+
// tool_result message is what keeps blocks paired.
|
|
1532
|
+
const dropSet = new Set(reclaim.drop);
|
|
1533
|
+
const truncateSet = new Set(reclaim.truncate);
|
|
1534
|
+
const rebuilt = [];
|
|
1535
|
+
for (let i = 0; i < conversation.length; i++) {
|
|
1536
|
+
if (dropSet.has(i)) {
|
|
1537
|
+
continue;
|
|
1565
1538
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1539
|
+
const message = conversation[i];
|
|
1540
|
+
if (truncateSet.has(i) && Array.isArray(message.content)) {
|
|
1541
|
+
rebuilt.push({
|
|
1542
|
+
...message,
|
|
1543
|
+
content: message.content.map((block) => block.type === "tool_result"
|
|
1544
|
+
? {
|
|
1545
|
+
...block,
|
|
1546
|
+
content: previewAnthropicToolResultText(typeof block.content === "string"
|
|
1547
|
+
? block.content
|
|
1548
|
+
: (JSON.stringify(block.content) ?? "")),
|
|
1549
|
+
}
|
|
1550
|
+
: block),
|
|
1578
1551
|
});
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
rebuilt.push(message);
|
|
1555
|
+
}
|
|
1556
|
+
if (dropSet.size > 0) {
|
|
1557
|
+
// Anthropic requires user/assistant alternation around tool blocks;
|
|
1558
|
+
// the note is a user turn placed immediately before the first
|
|
1559
|
+
// surviving assistant tool_use turn, which preserves it.
|
|
1560
|
+
let noteIndex = rebuilt.findIndex((m) => Array.isArray(m.content) &&
|
|
1561
|
+
m.content.some((b) => b.type === "tool_use" || b.type === "tool_result"));
|
|
1562
|
+
if (noteIndex < 0) {
|
|
1563
|
+
noteIndex = Math.min(1, rebuilt.length);
|
|
1564
|
+
}
|
|
1565
|
+
rebuilt.splice(noteIndex, 0, {
|
|
1566
|
+
role: "user",
|
|
1567
|
+
content: [{ type: "text", text: ANTHROPIC_ELISION_NOTE }],
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
return rebuilt;
|
|
1571
|
+
};
|
|
1572
|
+
const buildParams = (conversation) => {
|
|
1573
|
+
// Mid-turn discovery sync: search_tools (tools.discovery) hydrates
|
|
1574
|
+
// new tools into toolsRecord between steps; Claude only calls tools
|
|
1575
|
+
// declared in the request, so advertise them now.
|
|
1576
|
+
if (anthropicTools) {
|
|
1577
|
+
const declared = new Set(anthropicTools.map((t) => t.name));
|
|
1578
|
+
const hydrated = Object.fromEntries(Object.entries(toolsRecord).filter(([name]) => !declared.has(name)));
|
|
1579
|
+
if (Object.keys(hydrated).length > 0) {
|
|
1580
|
+
const extra = toNativeToolDeclarations(hydrated, "input_schema");
|
|
1581
|
+
if (extra && extra.length > 0) {
|
|
1582
|
+
anthropicTools = [...anthropicTools, ...extra];
|
|
1583
|
+
}
|
|
1579
1584
|
}
|
|
1580
|
-
conversation.length = 0;
|
|
1581
|
-
conversation.push(...rebuilt);
|
|
1582
1585
|
}
|
|
1583
1586
|
// Prompt-cache parity with the native Vertex+Claude path — rolling
|
|
1584
|
-
// history breakpoints, re-applied per step so the stable prefix
|
|
1585
|
-
//
|
|
1586
|
-
// tail. Budget respects markers upstream layers already placed
|
|
1587
|
-
// (system / last tool / message blocks) so the request never
|
|
1588
|
-
// exceeds Anthropic's four-marker cap. Pure: `conversation` itself
|
|
1589
|
-
// is never mutated, so re-counting per step stays stable.
|
|
1587
|
+
// history breakpoints, re-applied per step so the stable prefix stays
|
|
1588
|
+
// byte-identical while the breakpoint follows the growing tail.
|
|
1590
1589
|
const cacheMarkersUsed = countAnthropicCacheMarkers({
|
|
1591
1590
|
system: payload.system,
|
|
1592
1591
|
tools: anthropicTools,
|
|
1593
1592
|
messages: conversation,
|
|
1594
1593
|
});
|
|
1595
1594
|
const cachedConversation = applyAnthropicHistoryCacheBreakpoints(conversation, ANTHROPIC_MAX_CACHE_BREAKPOINTS - cacheMarkersUsed);
|
|
1596
|
-
// Registry-driven strip (Sonnet 5 / Opus 4.7+ / Fable 5 families)
|
|
1597
1595
|
const streamSamplingParams = resolveSamplingParams("anthropic", modelId, options.temperature !== undefined && options.temperature !== null
|
|
1598
1596
|
? { temperature: options.temperature }
|
|
1599
1597
|
: {}, "anthropic.executeStream");
|
|
1600
|
-
|
|
1598
|
+
return {
|
|
1601
1599
|
model: modelId,
|
|
1602
1600
|
messages: cachedConversation,
|
|
1603
1601
|
max_tokens: resolveClaudeMaxTokens(modelId, options.maxTokens),
|
|
@@ -1612,239 +1610,123 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1612
1610
|
...(anthropicToolChoice ? { tool_choice: anthropicToolChoice } : {}),
|
|
1613
1611
|
...(thinking ? { thinking } : {}),
|
|
1614
1612
|
};
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
// counts the billed output it already reported.
|
|
1633
|
-
let stepOutputTokens = 0;
|
|
1634
|
-
for await (const event of events) {
|
|
1635
|
-
if (event.type === "message_start") {
|
|
1636
|
-
totalInput += event.message.usage.input_tokens ?? 0;
|
|
1637
|
-
const startOutputTokens = event.message.usage.output_tokens ?? 0;
|
|
1638
|
-
totalOutput += startOutputTokens - stepOutputTokens;
|
|
1639
|
-
stepOutputTokens = startOutputTokens;
|
|
1640
|
-
// Anthropic reports cache reads/writes SEPARATELY from
|
|
1641
|
-
// input_tokens on the same message_start event — without these
|
|
1642
|
-
// the streaming path silently drops all cache accounting.
|
|
1643
|
-
totalCacheRead += event.message.usage.cache_read_input_tokens ?? 0;
|
|
1644
|
-
totalCacheWrite +=
|
|
1645
|
-
event.message.usage.cache_creation_input_tokens ?? 0;
|
|
1646
|
-
// Calibration signal for the in-turn guard: the FULL prompt size,
|
|
1647
|
-
// which on this path means uncached input plus both cache tiers.
|
|
1648
|
-
// Using input_tokens alone would read a cache-hit step as tiny and
|
|
1649
|
-
// let the guard drift far under the real cost.
|
|
1650
|
-
lastObservedPromptTokens =
|
|
1651
|
-
(event.message.usage.input_tokens ?? 0) +
|
|
1652
|
-
(event.message.usage.cache_read_input_tokens ?? 0) +
|
|
1653
|
-
(event.message.usage.cache_creation_input_tokens ?? 0);
|
|
1654
|
-
}
|
|
1655
|
-
else if (event.type === "content_block_start") {
|
|
1656
|
-
blockTypes.set(event.index, event.content_block.type);
|
|
1657
|
-
if (event.content_block.type === "tool_use") {
|
|
1658
|
-
toolAcc.set(event.index, {
|
|
1659
|
-
id: event.content_block.id,
|
|
1660
|
-
name: event.content_block.name,
|
|
1661
|
-
inputJson: "",
|
|
1662
|
-
});
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
else if (event.type === "content_block_delta") {
|
|
1666
|
-
const delta = event.delta;
|
|
1667
|
-
if (delta.type === "text_delta") {
|
|
1668
|
-
textAcc.set(event.index, (textAcc.get(event.index) ?? "") + delta.text);
|
|
1669
|
-
if (finalResultActive) {
|
|
1670
|
-
bufferedText += delta.text;
|
|
1671
|
-
}
|
|
1672
|
-
else {
|
|
1673
|
-
pushChunk({ content: delta.text });
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
else if (delta.type === "thinking_delta") {
|
|
1677
|
-
const acc = thinkingAcc.get(event.index) ?? {
|
|
1678
|
-
text: "",
|
|
1679
|
-
signature: "",
|
|
1680
|
-
};
|
|
1681
|
-
acc.text += delta.thinking;
|
|
1682
|
-
thinkingAcc.set(event.index, acc);
|
|
1683
|
-
// Reasoning rides the dedicated chunk channel; `content` stays
|
|
1684
|
-
// an always-present string so plain-text consumers are safe.
|
|
1685
|
-
pushChunk({ content: "", reasoning: delta.thinking });
|
|
1686
|
-
}
|
|
1687
|
-
else if (delta.type === "signature_delta") {
|
|
1688
|
-
const acc = thinkingAcc.get(event.index) ?? {
|
|
1689
|
-
text: "",
|
|
1690
|
-
signature: "",
|
|
1691
|
-
};
|
|
1692
|
-
acc.signature += delta.signature;
|
|
1693
|
-
thinkingAcc.set(event.index, acc);
|
|
1694
|
-
}
|
|
1695
|
-
else if (delta.type === "input_json_delta") {
|
|
1696
|
-
const acc = toolAcc.get(event.index);
|
|
1697
|
-
if (acc) {
|
|
1698
|
-
acc.inputJson += delta.partial_json;
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
else if (event.type === "message_delta") {
|
|
1703
|
-
stopReason = event.delta.stop_reason ?? stopReason;
|
|
1704
|
-
const cumulativeOutputTokens = event.usage?.output_tokens ?? stepOutputTokens;
|
|
1705
|
-
totalOutput += cumulativeOutputTokens - stepOutputTokens;
|
|
1706
|
-
stepOutputTokens = cumulativeOutputTokens;
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
lastStop = stopReason;
|
|
1710
|
-
// final_result is terminal: its arguments ARE the answer, so the turn
|
|
1711
|
-
// ends here and any tool calls issued alongside it are not executed
|
|
1712
|
-
// (parity with the native Vertex loops). It is never executed as a
|
|
1713
|
-
// tool, never recorded in toolsUsed, and never stored as a tool
|
|
1714
|
-
// execution — the pattern stays invisible to callers.
|
|
1715
|
-
if (finalResultActive) {
|
|
1716
|
-
const finalCall = [...toolAcc.values()].find((acc) => acc.name === FINAL_RESULT_TOOL_NAME);
|
|
1717
|
-
if (finalCall) {
|
|
1718
|
-
finalResultText = stringifyFinalResultInput(finalCall.inputJson);
|
|
1719
|
-
lastStop = "end_turn";
|
|
1720
|
-
logger.debug("[Anthropic] Extracted structured output from final_result tool (stream)", { chars: finalResultText.length });
|
|
1721
|
-
break;
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
if (stopReason !== "tool_use" || toolAcc.size === 0) {
|
|
1725
|
-
break;
|
|
1726
|
-
}
|
|
1727
|
-
// Replay this assistant turn (thinking + text + tool_use blocks, in
|
|
1728
|
-
// block order) then execute the requested tools and append their
|
|
1729
|
-
// results as a user turn — the native multi-step tool loop.
|
|
1730
|
-
const assistantBlocks = [];
|
|
1731
|
-
const orderedIndexes = [...blockTypes.keys()].sort((a, b) => a - b);
|
|
1732
|
-
for (const idx of orderedIndexes) {
|
|
1733
|
-
const type = blockTypes.get(idx);
|
|
1734
|
-
if (type === "thinking") {
|
|
1735
|
-
const acc = thinkingAcc.get(idx);
|
|
1736
|
-
if (acc && acc.text.length > 0) {
|
|
1737
|
-
assistantBlocks.push({
|
|
1738
|
-
type: "thinking",
|
|
1739
|
-
thinking: acc.text,
|
|
1740
|
-
signature: acc.signature,
|
|
1741
|
-
});
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
else if (type === "text") {
|
|
1745
|
-
const text = textAcc.get(idx);
|
|
1746
|
-
if (text && text.length > 0) {
|
|
1747
|
-
assistantBlocks.push({ type: "text", text });
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
else if (type === "tool_use") {
|
|
1751
|
-
const acc = toolAcc.get(idx);
|
|
1752
|
-
if (acc) {
|
|
1753
|
-
let input;
|
|
1754
|
-
try {
|
|
1755
|
-
input = acc.inputJson ? JSON.parse(acc.inputJson) : {};
|
|
1756
|
-
}
|
|
1757
|
-
catch {
|
|
1758
|
-
input = {};
|
|
1759
|
-
}
|
|
1760
|
-
assistantBlocks.push({
|
|
1761
|
-
type: "tool_use",
|
|
1762
|
-
id: acc.id,
|
|
1763
|
-
name: acc.name,
|
|
1764
|
-
input,
|
|
1765
|
-
});
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
conversation.push({ role: "assistant", content: assistantBlocks });
|
|
1770
|
-
const resultBlocks = [];
|
|
1771
|
-
const toolCallsForStorage = [];
|
|
1772
|
-
const toolResultsForStorage = [];
|
|
1773
|
-
for (const acc of toolAcc.values()) {
|
|
1774
|
-
let args;
|
|
1775
|
-
try {
|
|
1776
|
-
args = acc.inputJson
|
|
1777
|
-
? JSON.parse(acc.inputJson)
|
|
1778
|
-
: {};
|
|
1613
|
+
};
|
|
1614
|
+
const baseAdapter = createAnthropicLoopAdapter({
|
|
1615
|
+
client,
|
|
1616
|
+
maxSteps,
|
|
1617
|
+
toolsRecord,
|
|
1618
|
+
buildParams,
|
|
1619
|
+
planReclaim,
|
|
1620
|
+
noteObservedPromptTokens: (tokens) => {
|
|
1621
|
+
lastObservedPromptTokens = tokens;
|
|
1622
|
+
},
|
|
1623
|
+
...(finalResultActive
|
|
1624
|
+
? {
|
|
1625
|
+
finalResultToolName: FINAL_RESULT_TOOL_NAME,
|
|
1626
|
+
onTerminalResult: (text) => {
|
|
1627
|
+
finalResultText = text;
|
|
1628
|
+
logger.debug("[Anthropic] Extracted structured output from final_result tool (stream)", { chars: text.length });
|
|
1629
|
+
},
|
|
1779
1630
|
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1631
|
+
: {}),
|
|
1632
|
+
});
|
|
1633
|
+
// Wrapped rather than folded into the adapter: analytics emission and
|
|
1634
|
+
// tool-execution storage fire ONCE PER STEP in the loop this replaces,
|
|
1635
|
+
// and buildToolResultMessages is the only per-step hook — it receives
|
|
1636
|
+
// exactly that step's results. Doing this from the turn's final result
|
|
1637
|
+
// instead would batch every step's tools into one late write.
|
|
1638
|
+
const adapter = {
|
|
1639
|
+
...baseAdapter,
|
|
1640
|
+
buildToolResultMessages: (conversation, stepResult, toolResults) => {
|
|
1641
|
+
for (const result of toolResults) {
|
|
1642
|
+
toolsUsed.push(result.name);
|
|
1782
1643
|
}
|
|
1783
|
-
toolCallsForStorage.
|
|
1644
|
+
const toolCallsForStorage = toolResults.map((result) => ({
|
|
1784
1645
|
type: "tool-call",
|
|
1785
|
-
toolCallId:
|
|
1786
|
-
toolName:
|
|
1787
|
-
args,
|
|
1788
|
-
});
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
try {
|
|
1796
|
-
if (!tool?.execute) {
|
|
1797
|
-
throw new Error(`Tool not found: ${acc.name}`);
|
|
1646
|
+
toolCallId: result.id,
|
|
1647
|
+
toolName: result.name,
|
|
1648
|
+
args: result.args,
|
|
1649
|
+
}));
|
|
1650
|
+
const toolResultsForStorage = toolResults.map((result) => result.error
|
|
1651
|
+
? {
|
|
1652
|
+
type: "tool-result",
|
|
1653
|
+
toolCallId: result.id,
|
|
1654
|
+
toolName: result.name,
|
|
1655
|
+
error: result.error,
|
|
1798
1656
|
}
|
|
1799
|
-
|
|
1800
|
-
toolCallId: acc.id,
|
|
1801
|
-
messages: [],
|
|
1802
|
-
});
|
|
1803
|
-
toolResultsForStorage.push({
|
|
1657
|
+
: {
|
|
1804
1658
|
type: "tool-result",
|
|
1805
|
-
toolCallId:
|
|
1806
|
-
toolName:
|
|
1807
|
-
result,
|
|
1808
|
-
});
|
|
1809
|
-
resultBlocks.push({
|
|
1810
|
-
type: "tool_result",
|
|
1811
|
-
tool_use_id: acc.id,
|
|
1812
|
-
content: stringifyAnthropicToolOutput(result),
|
|
1659
|
+
toolCallId: result.id,
|
|
1660
|
+
toolName: result.name,
|
|
1661
|
+
result: result.output,
|
|
1813
1662
|
});
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1663
|
+
emitToolEndFromStepFinish(emitter, toolResultsForStorage.map((tr) => ({
|
|
1664
|
+
toolName: tr.toolName,
|
|
1665
|
+
result: "result" in tr ? tr.result : undefined,
|
|
1666
|
+
error: "error" in tr ? tr.error : undefined,
|
|
1667
|
+
})));
|
|
1668
|
+
this.handleToolExecutionStorage(toolCallsForStorage, toolResultsForStorage, options, new Date()).catch((storageErr) => {
|
|
1669
|
+
logger.warn("[AnthropicProvider] Failed to store tool executions", {
|
|
1670
|
+
provider: this.providerName,
|
|
1671
|
+
error: storageErr instanceof Error
|
|
1672
|
+
? storageErr.message
|
|
1673
|
+
: String(storageErr),
|
|
1822
1674
|
});
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1675
|
+
});
|
|
1676
|
+
return baseAdapter.buildToolResultMessages(conversation, stepResult, toolResults);
|
|
1677
|
+
},
|
|
1678
|
+
};
|
|
1679
|
+
// Presented in the shape the engine dispatches through. The engine
|
|
1680
|
+
// supplies `{ toolCallId, abortSignal }`; the loop this replaces also
|
|
1681
|
+
// supplied `messages: []`, so it is kept — a tool that reads it would
|
|
1682
|
+
// otherwise see undefined where it used to see an empty array.
|
|
1683
|
+
const engineTools = {};
|
|
1684
|
+
for (const [name, tool] of Object.entries(toolsRecord)) {
|
|
1685
|
+
const execute = tool.execute;
|
|
1686
|
+
if (!execute) {
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
engineTools[name] = {
|
|
1690
|
+
execute: async (args, opts) => {
|
|
1691
|
+
const ctx = opts;
|
|
1692
|
+
return execute(args, {
|
|
1693
|
+
toolCallId: ctx.toolCallId ?? "",
|
|
1694
|
+
...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
|
|
1695
|
+
messages: [],
|
|
1828
1696
|
});
|
|
1697
|
+
},
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
const { stream, resultPromise } = runAgenticLoop(adapter, payload.messages.slice(), {
|
|
1701
|
+
tools: engineTools,
|
|
1702
|
+
...(abortSignal ? { abortSignal } : {}),
|
|
1703
|
+
});
|
|
1704
|
+
// Structured turns buffer their text rather than streaming it: a caller
|
|
1705
|
+
// that passed a schema needs parseable JSON, and deltas emitted before
|
|
1706
|
+
// the model calls final_result would prefix the payload with prose.
|
|
1707
|
+
// Reasoning is forwarded either way — it is not part of the payload.
|
|
1708
|
+
const pump = (async () => {
|
|
1709
|
+
for await (const chunk of stream) {
|
|
1710
|
+
if (chunk.reasoning) {
|
|
1711
|
+
pushChunk({ content: "", reasoning: chunk.reasoning });
|
|
1712
|
+
}
|
|
1713
|
+
if (chunk.content) {
|
|
1714
|
+
if (finalResultActive) {
|
|
1715
|
+
bufferedText += chunk.content;
|
|
1716
|
+
}
|
|
1717
|
+
else {
|
|
1718
|
+
pushChunk({ content: chunk.content });
|
|
1719
|
+
}
|
|
1829
1720
|
}
|
|
1830
1721
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
logger.warn("[AnthropicProvider] Failed to store tool executions", {
|
|
1840
|
-
provider: this.providerName,
|
|
1841
|
-
error: storageErr instanceof Error
|
|
1842
|
-
? storageErr.message
|
|
1843
|
-
: String(storageErr),
|
|
1844
|
-
});
|
|
1845
|
-
});
|
|
1846
|
-
conversation.push({ role: "user", content: resultBlocks });
|
|
1847
|
-
}
|
|
1722
|
+
})();
|
|
1723
|
+
const result = await resultPromise;
|
|
1724
|
+
await pump;
|
|
1725
|
+
totalInput += result.usage.inputTokens;
|
|
1726
|
+
totalOutput += result.usage.outputTokens;
|
|
1727
|
+
totalCacheRead += result.usage.cacheReadTokens ?? 0;
|
|
1728
|
+
totalCacheWrite += result.usage.cacheWriteTokens ?? 0;
|
|
1729
|
+
lastStop = result.rawStopReason ?? lastStop;
|
|
1848
1730
|
resolveUsage(buildDeferredUsage());
|
|
1849
1731
|
resolveFinish(lastStop ?? "stop");
|
|
1850
1732
|
};
|
|
@@ -285,6 +285,9 @@ export function createAnthropicLoopAdapter(config) {
|
|
|
285
285
|
const finalText = terminal
|
|
286
286
|
? stringifyFinalResultInput(terminal.inputJson)
|
|
287
287
|
: text;
|
|
288
|
+
if (terminal) {
|
|
289
|
+
config.onTerminalResult?.(finalText);
|
|
290
|
+
}
|
|
288
291
|
return {
|
|
289
292
|
text: finalText,
|
|
290
293
|
...(reasoning ? { reasoning } : {}),
|
|
@@ -1001,7 +1001,16 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
1001
1001
|
if (wantsNativeJsonRequested && exclusionInForce) {
|
|
1002
1002
|
logger.warn("[GoogleAIStudio] Gemini does not support tools and JSON schema output simultaneously. Disabling tools for this request (generate()).");
|
|
1003
1003
|
}
|
|
1004
|
-
|
|
1004
|
+
// Both conjuncts, matching the warning directly above and the
|
|
1005
|
+
// stream path's gate. `isToolsSchemaExclusionInForce` answers "does
|
|
1006
|
+
// the tools/schema exclusion APPLY to this provider and model", and
|
|
1007
|
+
// is true for any Gemini request that has tools at all — it is not
|
|
1008
|
+
// "the exclusion is triggered". Testing `!exclusionInForce` alone
|
|
1009
|
+
// therefore made this branch reachable only when there were NO
|
|
1010
|
+
// tools, so every caller-supplied tool was dropped on the generate
|
|
1011
|
+
// path whether or not structured output was ever requested.
|
|
1012
|
+
if (shouldUseTools &&
|
|
1013
|
+
!(wantsNativeJsonRequested && exclusionInForce)) {
|
|
1005
1014
|
const tools = options.tools || {};
|
|
1006
1015
|
if (Object.keys(tools).length > 0) {
|
|
1007
1016
|
const result = toNativeToolDeclarations(tools, "functionDeclarations");
|
|
@@ -151,6 +151,18 @@ export type AnthropicLoopAdapterConfig = {
|
|
|
151
151
|
* ordinary zero-tool-calls exit.
|
|
152
152
|
*/
|
|
153
153
|
finalResultToolName?: string;
|
|
154
|
+
/**
|
|
155
|
+
* Called with the terminal tool's payload when one was actually detected.
|
|
156
|
+
*
|
|
157
|
+
* The caller cannot infer this from the turn's result. A structured turn
|
|
158
|
+
* ends with the payload in `text` when the model called the terminal tool,
|
|
159
|
+
* and with ordinary prose in `text` when it ignored the instruction and
|
|
160
|
+
* answered directly — the two are indistinguishable downstream, yet they
|
|
161
|
+
* are handled differently: the payload is delivered as the answer, while
|
|
162
|
+
* prose is delivered from the caller's own buffer. Comparing strings to
|
|
163
|
+
* tell them apart would be guesswork, so the adapter says which happened.
|
|
164
|
+
*/
|
|
165
|
+
onTerminalResult?: (text: string) => void;
|
|
154
166
|
toolFailureBreaker?: AgenticLoopToolFailureBreaker;
|
|
155
167
|
/**
|
|
156
168
|
* In-turn context reclaim, run once per step before the request is built.
|