@juspay/neurolink 10.10.0 → 10.10.1

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.
@@ -26,6 +26,7 @@ import { DEFAULT_MAX_STEPS, DEFAULT_WRAPUP_TIME_LEAD_MS, } from "../constants.js
26
26
  import { createStepBudgetGuard, estimateFixedOverheadTokens, } from "../../context/stepBudgetGuard.js";
27
27
  import { isTemperatureDeprecatedError, isSchemaComplexityError, isToolsSchemaConflictError, isToolsSchemaExclusionInForce, } from "./structuredOutputPolicy.js";
28
28
  import { coerceJsonToSchema } from "../../utils/json/coerce.js";
29
+ import { convertZodToJsonSchema } from "../../utils/schemaConversion.js";
29
30
  import { NoObjectGeneratedError } from "../../utils/generationErrors.js";
30
31
  import { Output, stepCountIs } from "../../utils/tool.js";
31
32
  import { generateText } from "../../utils/generation.js";
@@ -105,11 +106,14 @@ export function resolveTurnBudget(options, turnStartMs) {
105
106
  * `providerOptions:` spreads in the args literal would silently clobber each
106
107
  * other (object spread does not deep-merge).
107
108
  */
108
- function buildProviderOptions(options, isGoogleProvider, callerTimeoutMs) {
109
+ function buildProviderOptions(options, isGoogleProvider, callerTimeoutMs, finalResultSchema) {
109
110
  const providerOptions = {};
110
111
  if (callerTimeoutMs !== undefined) {
111
112
  providerOptions.neurolink = { timeoutMs: callerTimeoutMs };
112
113
  }
114
+ if (finalResultSchema) {
115
+ providerOptions.anthropic = { finalResultSchema };
116
+ }
113
117
  if (options.thinkingConfig?.enabled && isGoogleProvider) {
114
118
  // Gemini 3 uses thinkingLevel; Gemini 2.5 uses thinkingBudget.
115
119
  providerOptions.google = {
@@ -236,7 +240,22 @@ export class GenerationHandler {
236
240
  const prepareStep = options.prepareStep;
237
241
  const { callerTimeoutMs, turnBudgetMs, wrapupLeadMs, turnDeadline } = resolveTurnBudget(options, turnStartMs);
238
242
  let wrapupForced = false;
239
- const providerOptions = buildProviderOptions(options, isGoogleProvider, callerTimeoutMs);
243
+ // The native Anthropic Messages surface cannot combine AI-SDK structured
244
+ // output with tools (see structuredOutputPolicy — experimental_output
245
+ // replaces the tools array), so `useStructuredOutput` is false above and
246
+ // the schema would simply be dropped for every agent/MCP turn. Hand the
247
+ // JSON Schema to the provider instead: it appends an additive
248
+ // `final_result` tool and returns the answer as that tool's arguments,
249
+ // keeping the real tools callable. Bedrock is deliberately excluded — it
250
+ // runs on the third-party @ai-sdk/amazon-bedrock model, which has no such
251
+ // handling.
252
+ const finalResultSchema = this.providerName === "anthropic" &&
253
+ !!options.schema &&
254
+ shouldUseTools &&
255
+ Object.keys(tools).length > 0
256
+ ? convertZodToJsonSchema(options.schema)
257
+ : undefined;
258
+ const providerOptions = buildProviderOptions(options, isGoogleProvider, callerTimeoutMs, finalResultSchema);
240
259
  // Hoist system-role messages into generateText's top-level `system` option
241
260
  // rather than passing them inside `messages` (deprecated by the AI SDK,
242
261
  // rejected in v7). See extractSystemMessages for the rationale. (#1024)
@@ -23,6 +23,14 @@ export declare function isGeminiProvider(providerName: string, modelName: string
23
23
  * experimental_output + tools silently drops tool_use blocks on this surface, so
24
24
  * structured output must be disabled when tools are active. Vertex+Claude is NOT
25
25
  * matched here (different transport, no conflict).
26
+ *
27
+ * Being excluded here no longer means the schema is LOST for provider
28
+ * "anthropic": GenerationHandler forwards the JSON Schema to the provider via
29
+ * `providerOptions.anthropic.finalResultSchema`, and the provider appends an
30
+ * additive `final_result` tool (see providers/anthropic/structuredOutput.ts) —
31
+ * schema enforcement without giving up tool calling. "bedrock" has no such
32
+ * handling (it runs on the third-party @ai-sdk/amazon-bedrock model) and still
33
+ * falls back to text-mode coercion.
26
34
  */
27
35
  export declare function isNativeAnthropicProvider(providerName: string): boolean;
28
36
  /**
@@ -33,6 +33,14 @@ export function isGeminiProvider(providerName, modelName) {
33
33
  * experimental_output + tools silently drops tool_use blocks on this surface, so
34
34
  * structured output must be disabled when tools are active. Vertex+Claude is NOT
35
35
  * matched here (different transport, no conflict).
36
+ *
37
+ * Being excluded here no longer means the schema is LOST for provider
38
+ * "anthropic": GenerationHandler forwards the JSON Schema to the provider via
39
+ * `providerOptions.anthropic.finalResultSchema`, and the provider appends an
40
+ * additive `final_result` tool (see providers/anthropic/structuredOutput.ts) —
41
+ * schema enforcement without giving up tool calling. "bedrock" has no such
42
+ * handling (it runs on the third-party @ai-sdk/amazon-bedrock model) and still
43
+ * falls back to text-mode coercion.
36
44
  */
37
45
  export function isNativeAnthropicProvider(providerName) {
38
46
  return providerName === "anthropic" || providerName === "bedrock";
@@ -26,6 +26,7 @@ import { DEFAULT_MAX_STEPS, DEFAULT_WRAPUP_TIME_LEAD_MS, } from "../constants.js
26
26
  import { createStepBudgetGuard, estimateFixedOverheadTokens, } from "../../context/stepBudgetGuard.js";
27
27
  import { isTemperatureDeprecatedError, isSchemaComplexityError, isToolsSchemaConflictError, isToolsSchemaExclusionInForce, } from "./structuredOutputPolicy.js";
28
28
  import { coerceJsonToSchema } from "../../utils/json/coerce.js";
29
+ import { convertZodToJsonSchema } from "../../utils/schemaConversion.js";
29
30
  import { NoObjectGeneratedError } from "../../utils/generationErrors.js";
30
31
  import { Output, stepCountIs } from "../../utils/tool.js";
31
32
  import { generateText } from "../../utils/generation.js";
@@ -105,11 +106,14 @@ export function resolveTurnBudget(options, turnStartMs) {
105
106
  * `providerOptions:` spreads in the args literal would silently clobber each
106
107
  * other (object spread does not deep-merge).
107
108
  */
108
- function buildProviderOptions(options, isGoogleProvider, callerTimeoutMs) {
109
+ function buildProviderOptions(options, isGoogleProvider, callerTimeoutMs, finalResultSchema) {
109
110
  const providerOptions = {};
110
111
  if (callerTimeoutMs !== undefined) {
111
112
  providerOptions.neurolink = { timeoutMs: callerTimeoutMs };
112
113
  }
114
+ if (finalResultSchema) {
115
+ providerOptions.anthropic = { finalResultSchema };
116
+ }
113
117
  if (options.thinkingConfig?.enabled && isGoogleProvider) {
114
118
  // Gemini 3 uses thinkingLevel; Gemini 2.5 uses thinkingBudget.
115
119
  providerOptions.google = {
@@ -236,7 +240,22 @@ export class GenerationHandler {
236
240
  const prepareStep = options.prepareStep;
237
241
  const { callerTimeoutMs, turnBudgetMs, wrapupLeadMs, turnDeadline } = resolveTurnBudget(options, turnStartMs);
238
242
  let wrapupForced = false;
239
- const providerOptions = buildProviderOptions(options, isGoogleProvider, callerTimeoutMs);
243
+ // The native Anthropic Messages surface cannot combine AI-SDK structured
244
+ // output with tools (see structuredOutputPolicy — experimental_output
245
+ // replaces the tools array), so `useStructuredOutput` is false above and
246
+ // the schema would simply be dropped for every agent/MCP turn. Hand the
247
+ // JSON Schema to the provider instead: it appends an additive
248
+ // `final_result` tool and returns the answer as that tool's arguments,
249
+ // keeping the real tools callable. Bedrock is deliberately excluded — it
250
+ // runs on the third-party @ai-sdk/amazon-bedrock model, which has no such
251
+ // handling.
252
+ const finalResultSchema = this.providerName === "anthropic" &&
253
+ !!options.schema &&
254
+ shouldUseTools &&
255
+ Object.keys(tools).length > 0
256
+ ? convertZodToJsonSchema(options.schema)
257
+ : undefined;
258
+ const providerOptions = buildProviderOptions(options, isGoogleProvider, callerTimeoutMs, finalResultSchema);
240
259
  // Hoist system-role messages into generateText's top-level `system` option
241
260
  // rather than passing them inside `messages` (deprecated by the AI SDK,
242
261
  // rejected in v7). See extractSystemMessages for the rationale. (#1024)
@@ -23,6 +23,14 @@ export declare function isGeminiProvider(providerName: string, modelName: string
23
23
  * experimental_output + tools silently drops tool_use blocks on this surface, so
24
24
  * structured output must be disabled when tools are active. Vertex+Claude is NOT
25
25
  * matched here (different transport, no conflict).
26
+ *
27
+ * Being excluded here no longer means the schema is LOST for provider
28
+ * "anthropic": GenerationHandler forwards the JSON Schema to the provider via
29
+ * `providerOptions.anthropic.finalResultSchema`, and the provider appends an
30
+ * additive `final_result` tool (see providers/anthropic/structuredOutput.ts) —
31
+ * schema enforcement without giving up tool calling. "bedrock" has no such
32
+ * handling (it runs on the third-party @ai-sdk/amazon-bedrock model) and still
33
+ * falls back to text-mode coercion.
26
34
  */
27
35
  export declare function isNativeAnthropicProvider(providerName: string): boolean;
28
36
  /**
@@ -33,6 +33,14 @@ export function isGeminiProvider(providerName, modelName) {
33
33
  * experimental_output + tools silently drops tool_use blocks on this surface, so
34
34
  * structured output must be disabled when tools are active. Vertex+Claude is NOT
35
35
  * matched here (different transport, no conflict).
36
+ *
37
+ * Being excluded here no longer means the schema is LOST for provider
38
+ * "anthropic": GenerationHandler forwards the JSON Schema to the provider via
39
+ * `providerOptions.anthropic.finalResultSchema`, and the provider appends an
40
+ * additive `final_result` tool (see providers/anthropic/structuredOutput.ts) —
41
+ * schema enforcement without giving up tool calling. "bedrock" has no such
42
+ * handling (it runs on the third-party @ai-sdk/amazon-bedrock model) and still
43
+ * falls back to text-mode coercion.
36
44
  */
37
45
  export function isNativeAnthropicProvider(providerName) {
38
46
  return providerName === "anthropic" || providerName === "bedrock";
@@ -30,6 +30,7 @@ import { toAnthropicImageBlock, fileToAnthropicBlock, } from "../anthropicImageB
30
30
  import { resolveSamplingParams } from "../../models/modelRegistry.js";
31
31
  import { createChunkQueue, createDeferredAnalytics, stringifyToolInput, } from "../openaiChatCompletionsClient.js";
32
32
  import { ANTHROPIC_BETA_HEADERS } from "./constants.js";
33
+ import { appendFinalResultInstruction, appendFinalResultTool, FINAL_RESULT_TOOL_NAME, stringifyFinalResultInput, } from "./structuredOutput.js";
33
34
  // AnthropicProviderConfig is imported from types/providers.ts
34
35
  // Re-export for backward compatibility
35
36
  // Configuration helpers - now using consolidated utility
@@ -1063,7 +1064,11 @@ export class AnthropicProvider extends BaseProvider {
1063
1064
  supportedUrls: {},
1064
1065
  doGenerate: async (options) => {
1065
1066
  await refreshAuth();
1066
- const { system, messages } = messagesToAnthropic(options.prompt);
1067
+ const built = messagesToAnthropic(options.prompt);
1068
+ const messages = built.messages;
1069
+ // `let`: the additive structured-output path below appends the
1070
+ // final_result instruction to the system prompt.
1071
+ let system = built.system;
1067
1072
  let tools = (options.tools ?? [])
1068
1073
  .filter((t) => t.type === "function")
1069
1074
  .map((t) => {
@@ -1107,6 +1112,24 @@ export class AnthropicProvider extends BaseProvider {
1107
1112
  ];
1108
1113
  toolChoice = { type: "tool", name: jsonTool };
1109
1114
  }
1115
+ // Additive structured output: when the caller wants a schema AND real
1116
+ // tools, the forced-json path above cannot be used (it replaces the
1117
+ // tools array), and the AI-SDK experimental_output path is excluded
1118
+ // for this surface by structuredOutputPolicy. GenerationHandler hands
1119
+ // the JSON Schema down here instead, and we APPEND a `final_result`
1120
+ // tool — tool_choice stays auto, so every real tool keeps working and
1121
+ // the model self-selects final_result when it is ready to answer.
1122
+ const finalResultSchema = options.providerOptions?.anthropic
1123
+ ?.finalResultSchema;
1124
+ let finalResultActive = false;
1125
+ if (!jsonTool && finalResultSchema) {
1126
+ const appended = appendFinalResultTool(tools, finalResultSchema);
1127
+ tools = appended.tools;
1128
+ finalResultActive = appended.applied;
1129
+ if (appended.applied) {
1130
+ system = appendFinalResultInstruction(system);
1131
+ }
1132
+ }
1110
1133
  // Extended thinking passthrough (providerOptions.anthropic.thinking).
1111
1134
  const thinking = options.providerOptions?.anthropic?.thinking;
1112
1135
  // Prompt-cache parity with the native Vertex+Claude path: upstream
@@ -1179,6 +1202,7 @@ export class AnthropicProvider extends BaseProvider {
1179
1202
  timeoutController?.cleanup();
1180
1203
  }
1181
1204
  const content = [];
1205
+ let finalResultText;
1182
1206
  for (const block of response.content) {
1183
1207
  if (block.type === "thinking") {
1184
1208
  content.push({ type: "reasoning", text: block.thinking });
@@ -1198,6 +1222,12 @@ export class AnthropicProvider extends BaseProvider {
1198
1222
  text: stringifyToolInput(block.input),
1199
1223
  });
1200
1224
  }
1225
+ else if (finalResultActive &&
1226
+ block.name === FINAL_RESULT_TOOL_NAME) {
1227
+ // Internal pattern: never surfaced as a tool call. Its arguments
1228
+ // ARE the structured answer.
1229
+ finalResultText = stringifyToolInput(block.input);
1230
+ }
1201
1231
  else {
1202
1232
  content.push({
1203
1233
  type: "tool-call",
@@ -1208,12 +1238,29 @@ export class AnthropicProvider extends BaseProvider {
1208
1238
  }
1209
1239
  }
1210
1240
  }
1241
+ // final_result is terminal — parity with the native Claude-on-Vertex
1242
+ // and Gemini loops, which break out of the tool loop the moment it
1243
+ // arrives. Reasoning blocks are kept; any prose preamble and any tool
1244
+ // calls issued alongside it are dropped so `text` is exactly the
1245
+ // structured payload and the AI-SDK loop stops here.
1246
+ if (finalResultText !== undefined) {
1247
+ const reasoning = content.filter((part) => part.type === "reasoning");
1248
+ content.length = 0;
1249
+ content.push(...reasoning, { type: "text", text: finalResultText });
1250
+ logger.debug("[Anthropic] Extracted structured output from final_result tool (generate)", { chars: finalResultText.length });
1251
+ }
1211
1252
  const cacheRead = response.usage.cache_read_input_tokens ?? 0;
1212
1253
  const cacheWrite = response.usage.cache_creation_input_tokens ?? 0;
1213
1254
  return {
1214
1255
  content,
1215
1256
  finishReason: {
1216
- unified: mapAnthropicStopReason(response.stop_reason),
1257
+ // A final_result call ends the turn: the provider reports
1258
+ // stop_reason "tool_use", but no tool call is surfaced, so
1259
+ // reporting "tool-calls" would misread as a step-capped turn.
1260
+ // `raw` still carries the provider's verbatim stop_reason.
1261
+ unified: finalResultText !== undefined
1262
+ ? "stop"
1263
+ : mapAnthropicStopReason(response.stop_reason),
1217
1264
  raw: response.stop_reason ?? "stop",
1218
1265
  },
1219
1266
  usage: {
@@ -1341,6 +1388,9 @@ export class AnthropicProvider extends BaseProvider {
1341
1388
  let anthropicTools;
1342
1389
  let payload;
1343
1390
  let shouldUseTools;
1391
+ // True once the additive `final_result` tool is in the request — the
1392
+ // streaming twin of the doGenerate path above.
1393
+ let finalResultActive = false;
1344
1394
  try {
1345
1395
  // options.tools is pre-merged by BaseProvider.stream() with base tools
1346
1396
  // (MCP/built-in) + user-provided tools (RAG, etc.)
@@ -1355,6 +1405,18 @@ export class AnthropicProvider extends BaseProvider {
1355
1405
  // convert to the Anthropic Messages payload (system + content blocks).
1356
1406
  const built = await this.buildMessagesForStream(options);
1357
1407
  payload = messagesToAnthropic(built);
1408
+ // Schema + tools: append final_result rather than pinning tool_choice to
1409
+ // a json tool, so the real tools stay callable for the whole turn.
1410
+ // Unlike generate, no plumbing is needed — this is a native loop, so the
1411
+ // caller's Zod/JSON schema is right here on the options.
1412
+ if (options.schema && anthropicTools && anthropicTools.length > 0) {
1413
+ const appended = appendFinalResultTool(anthropicTools, convertZodToJsonSchema(options.schema));
1414
+ anthropicTools = appended.tools;
1415
+ finalResultActive = appended.applied;
1416
+ if (appended.applied) {
1417
+ payload.system = appendFinalResultInstruction(payload.system);
1418
+ }
1419
+ }
1358
1420
  }
1359
1421
  catch (setupErr) {
1360
1422
  timeoutController?.cleanup();
@@ -1439,6 +1501,14 @@ export class AnthropicProvider extends BaseProvider {
1439
1501
  ...(totalCacheRead > 0 ? { cacheReadTokens: totalCacheRead } : {}),
1440
1502
  ...(totalCacheWrite > 0 ? { cacheCreationTokens: totalCacheWrite } : {}),
1441
1503
  });
1504
+ // Structured-output turns are delivered as ONE chunk, not incrementally:
1505
+ // a caller that passed a schema needs parseable JSON, and text deltas
1506
+ // emitted before the model calls final_result would prefix the payload
1507
+ // with prose and break every JSON.parse on the consumer side. Same
1508
+ // contract as the native Vertex loops. Non-schema streams are untouched
1509
+ // and stay fully incremental.
1510
+ let bufferedText = "";
1511
+ let finalResultText;
1442
1512
  const runLoop = async () => {
1443
1513
  const conversation = payload.messages.slice();
1444
1514
  for (let step = 0; step < maxSteps; step++) {
@@ -1532,7 +1602,12 @@ export class AnthropicProvider extends BaseProvider {
1532
1602
  const delta = event.delta;
1533
1603
  if (delta.type === "text_delta") {
1534
1604
  textAcc.set(event.index, (textAcc.get(event.index) ?? "") + delta.text);
1535
- pushChunk({ content: delta.text });
1605
+ if (finalResultActive) {
1606
+ bufferedText += delta.text;
1607
+ }
1608
+ else {
1609
+ pushChunk({ content: delta.text });
1610
+ }
1536
1611
  }
1537
1612
  else if (delta.type === "thinking_delta") {
1538
1613
  const acc = thinkingAcc.get(event.index) ?? {
@@ -1568,6 +1643,20 @@ export class AnthropicProvider extends BaseProvider {
1568
1643
  }
1569
1644
  }
1570
1645
  lastStop = stopReason;
1646
+ // final_result is terminal: its arguments ARE the answer, so the turn
1647
+ // ends here and any tool calls issued alongside it are not executed
1648
+ // (parity with the native Vertex loops). It is never executed as a
1649
+ // tool, never recorded in toolsUsed, and never stored as a tool
1650
+ // execution — the pattern stays invisible to callers.
1651
+ if (finalResultActive) {
1652
+ const finalCall = [...toolAcc.values()].find((acc) => acc.name === FINAL_RESULT_TOOL_NAME);
1653
+ if (finalCall) {
1654
+ finalResultText = stringifyFinalResultInput(finalCall.inputJson);
1655
+ lastStop = "end_turn";
1656
+ logger.debug("[Anthropic] Extracted structured output from final_result tool (stream)", { chars: finalResultText.length });
1657
+ break;
1658
+ }
1659
+ }
1571
1660
  if (stopReason !== "tool_use" || toolAcc.size === 0) {
1572
1661
  break;
1573
1662
  }
@@ -1710,6 +1799,19 @@ export class AnthropicProvider extends BaseProvider {
1710
1799
  throw this.formatProviderError(error);
1711
1800
  })
1712
1801
  .finally(() => {
1802
+ // Deliver the buffered structured-output turn: `finalResultText` when
1803
+ // the model called final_result, otherwise the prose it produced
1804
+ // instead — never nothing, so a model that ignores the instruction
1805
+ // degrades to today's plain-text behaviour rather than an empty
1806
+ // stream. In `finally` so a turn that dies mid-loop still surfaces
1807
+ // the text it had already buffered, exactly as the unbuffered path
1808
+ // surfaces its partial deltas.
1809
+ if (finalResultActive) {
1810
+ const output = finalResultText ?? bufferedText;
1811
+ if (output.length > 0) {
1812
+ pushChunk({ content: output });
1813
+ }
1814
+ }
1713
1815
  timeoutController?.cleanup();
1714
1816
  pushChunk({ done: true });
1715
1817
  });
@@ -0,0 +1,58 @@
1
+ import type Anthropic from "@anthropic-ai/sdk";
2
+ /**
3
+ * Additive structured output for the native Anthropic Messages API.
4
+ *
5
+ * Anthropic has no `response_format`, so a schema has to be expressed as a
6
+ * tool. The provider's pre-existing `responseFormat` path does that by
7
+ * REPLACING the tools array with a single json tool and pinning `tool_choice`
8
+ * to it — correct for a schema-only call, but mutually exclusive with real
9
+ * tools, so agent/MCP turns that pass both silently lost the schema.
10
+ *
11
+ * The additive pattern here APPENDS a `final_result` tool to the caller's
12
+ * tools and leaves `tool_choice` on auto: the model keeps calling real tools
13
+ * for as long as it needs, then emits its answer as `final_result` arguments
14
+ * that already conform to the schema. This mirrors the native
15
+ * Claude-on-Vertex loop in `googleVertex/client.ts`, which has used the same
16
+ * tool name, description, and instruction wording since it shipped.
17
+ */
18
+ /** Internal tool name — filtered out of every returned tool call / execution. */
19
+ export declare const FINAL_RESULT_TOOL_NAME = "final_result";
20
+ /** Appended to the system prompt whenever the final_result tool is in play. */
21
+ export declare const FINAL_RESULT_INSTRUCTION = "\n\nIMPORTANT: You MUST call the 'final_result' tool to return your response in the required structured format. Do not respond with plain text - always use the final_result tool.";
22
+ /**
23
+ * Build the `final_result` tool definition from a JSON Schema.
24
+ *
25
+ * `$ref`s are inlined and `$schema` dropped — Anthropic's `input_schema` must
26
+ * be a self-contained object schema. Schemas that are not object-rooted (a
27
+ * bare array/string schema) are wrapped so `input_schema.type` is always
28
+ * "object", which the Messages API requires.
29
+ */
30
+ export declare function buildFinalResultTool(jsonSchema: Record<string, unknown>): Anthropic.Messages.Tool;
31
+ /**
32
+ * Append `final_result` to an Anthropic tool list.
33
+ *
34
+ * Returns a NEW array so the caller's tool list is never mutated, and reports
35
+ * `applied: false` (with the list unchanged) when the pattern must not run:
36
+ * there are no real tools to preserve, or the caller already exposes a tool of
37
+ * that name — shadowing a caller's tool would break their turn.
38
+ */
39
+ export declare function appendFinalResultTool(tools: Anthropic.Messages.Tool[] | undefined, jsonSchema: Record<string, unknown>): {
40
+ tools: Anthropic.Messages.Tool[] | undefined;
41
+ applied: boolean;
42
+ };
43
+ /**
44
+ * Append the final_result instruction to an Anthropic `system` value.
45
+ *
46
+ * The block-array form gets a NEW trailing block rather than an edit to the
47
+ * existing one: rewriting a block that carries a `cache_control` marker would
48
+ * change the cached prefix and invalidate the prompt cache on every turn.
49
+ */
50
+ export declare function appendFinalResultInstruction(system: string | Anthropic.Messages.TextBlockParam[] | undefined): string | Anthropic.Messages.TextBlockParam[];
51
+ /**
52
+ * Canonical JSON text for a `final_result` payload.
53
+ *
54
+ * Accepts the raw accumulated `input_json` from a stream so a payload
55
+ * truncated by the token cap is still returned verbatim — the caller's
56
+ * coercion layer can repair it, whereas dropping it loses the whole answer.
57
+ */
58
+ export declare function stringifyFinalResultInput(inputJson: string): string;
@@ -0,0 +1,98 @@
1
+ import { logger } from "../../utils/logger.js";
2
+ import { inlineJsonSchema } from "../../utils/schemaConversion.js";
3
+ /**
4
+ * Additive structured output for the native Anthropic Messages API.
5
+ *
6
+ * Anthropic has no `response_format`, so a schema has to be expressed as a
7
+ * tool. The provider's pre-existing `responseFormat` path does that by
8
+ * REPLACING the tools array with a single json tool and pinning `tool_choice`
9
+ * to it — correct for a schema-only call, but mutually exclusive with real
10
+ * tools, so agent/MCP turns that pass both silently lost the schema.
11
+ *
12
+ * The additive pattern here APPENDS a `final_result` tool to the caller's
13
+ * tools and leaves `tool_choice` on auto: the model keeps calling real tools
14
+ * for as long as it needs, then emits its answer as `final_result` arguments
15
+ * that already conform to the schema. This mirrors the native
16
+ * Claude-on-Vertex loop in `googleVertex/client.ts`, which has used the same
17
+ * tool name, description, and instruction wording since it shipped.
18
+ */
19
+ /** Internal tool name — filtered out of every returned tool call / execution. */
20
+ export const FINAL_RESULT_TOOL_NAME = "final_result";
21
+ const FINAL_RESULT_TOOL_DESCRIPTION = "Return the final structured result. You MUST call this tool when you have gathered all information and are ready to provide the final answer. The arguments should contain the structured data matching the expected schema.";
22
+ /** Appended to the system prompt whenever the final_result tool is in play. */
23
+ export const FINAL_RESULT_INSTRUCTION = "\n\nIMPORTANT: You MUST call the 'final_result' tool to return your response in the required structured format. Do not respond with plain text - always use the final_result tool.";
24
+ /**
25
+ * Build the `final_result` tool definition from a JSON Schema.
26
+ *
27
+ * `$ref`s are inlined and `$schema` dropped — Anthropic's `input_schema` must
28
+ * be a self-contained object schema. Schemas that are not object-rooted (a
29
+ * bare array/string schema) are wrapped so `input_schema.type` is always
30
+ * "object", which the Messages API requires.
31
+ */
32
+ export function buildFinalResultTool(jsonSchema) {
33
+ const inlined = inlineJsonSchema({ ...jsonSchema });
34
+ delete inlined.$schema;
35
+ const properties = inlined.properties;
36
+ const input_schema = {
37
+ type: "object",
38
+ properties: properties ?? inlined,
39
+ required: Array.isArray(inlined.required) ? inlined.required : [],
40
+ };
41
+ return {
42
+ name: FINAL_RESULT_TOOL_NAME,
43
+ description: FINAL_RESULT_TOOL_DESCRIPTION,
44
+ input_schema,
45
+ };
46
+ }
47
+ /**
48
+ * Append `final_result` to an Anthropic tool list.
49
+ *
50
+ * Returns a NEW array so the caller's tool list is never mutated, and reports
51
+ * `applied: false` (with the list unchanged) when the pattern must not run:
52
+ * there are no real tools to preserve, or the caller already exposes a tool of
53
+ * that name — shadowing a caller's tool would break their turn.
54
+ */
55
+ export function appendFinalResultTool(tools, jsonSchema) {
56
+ if (!tools || tools.length === 0) {
57
+ return { tools, applied: false };
58
+ }
59
+ if (tools.some((tool) => tool.name === FINAL_RESULT_TOOL_NAME)) {
60
+ logger.warn("[Anthropic] A caller tool is already named 'final_result'; skipping the additive structured-output tool");
61
+ return { tools, applied: false };
62
+ }
63
+ // Appended LAST so any cache_control breakpoint an upstream layer placed on
64
+ // the previously-last tool keeps marking the same prefix boundary.
65
+ return { tools: [...tools, buildFinalResultTool(jsonSchema)], applied: true };
66
+ }
67
+ /**
68
+ * Append the final_result instruction to an Anthropic `system` value.
69
+ *
70
+ * The block-array form gets a NEW trailing block rather than an edit to the
71
+ * existing one: rewriting a block that carries a `cache_control` marker would
72
+ * change the cached prefix and invalidate the prompt cache on every turn.
73
+ */
74
+ export function appendFinalResultInstruction(system) {
75
+ if (system === undefined) {
76
+ return FINAL_RESULT_INSTRUCTION.trim();
77
+ }
78
+ if (typeof system === "string") {
79
+ return system + FINAL_RESULT_INSTRUCTION;
80
+ }
81
+ return [...system, { type: "text", text: FINAL_RESULT_INSTRUCTION.trim() }];
82
+ }
83
+ /**
84
+ * Canonical JSON text for a `final_result` payload.
85
+ *
86
+ * Accepts the raw accumulated `input_json` from a stream so a payload
87
+ * truncated by the token cap is still returned verbatim — the caller's
88
+ * coercion layer can repair it, whereas dropping it loses the whole answer.
89
+ */
90
+ export function stringifyFinalResultInput(inputJson) {
91
+ try {
92
+ return JSON.stringify(JSON.parse(inputJson || "{}"));
93
+ }
94
+ catch {
95
+ return inputJson;
96
+ }
97
+ }
98
+ //# sourceMappingURL=structuredOutput.js.map
@@ -269,6 +269,11 @@ export type GenerateOptions = {
269
269
  * (see `coerceJsonToSchema`), and `disableTools: true` remains available as
270
270
  * an explicit override.
271
271
  *
272
+ * On the native Anthropic Messages surface (provider "anthropic", including
273
+ * via a proxy) tools + schema are honored through an internal `final_result`
274
+ * tool the model calls with the structured answer — invisible to callers: it
275
+ * never appears in `toolCalls` / `toolExecutions`.
276
+ *
272
277
  * @example
273
278
  * ```typescript
274
279
  * // ✅ Vertex + Claude: tools AND schema together are fully supported
@@ -278,6 +283,12 @@ export type GenerateOptions = {
278
283
  * model: "claude-sonnet-4-6",
279
284
  * });
280
285
  *
286
+ * // ✅ Direct Anthropic + tools: schema honored via the final_result tool
287
+ * const result = await neurolink.generate({
288
+ * schema: MySchema,
289
+ * provider: "anthropic",
290
+ * });
291
+ *
281
292
  * // ✅ Gemini + tools: SDK auto-falls back to coerced text-mode JSON
282
293
  * const result = await neurolink.generate({
283
294
  * schema: MySchema,