@agent-native/core 0.175.3 → 0.175.4-nightly-20260826190205

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.
Files changed (36) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/server/plugins/agent-chat.ts +3 -0
  3. package/corpus/templates/content/actions/_linked-local-document-edit.ts +167 -0
  4. package/corpus/templates/content/actions/edit-document.ts +181 -61
  5. package/corpus/templates/content/actions/import-content-source.ts +33 -1
  6. package/corpus/templates/content/actions/update-document.ts +2 -0
  7. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +68 -0
  8. package/corpus/templates/content/app/components/editor/LinkedLocalDocumentAgentBridge.tsx +245 -0
  9. package/corpus/templates/content/app/lib/local-content-source-files.ts +96 -9
  10. package/corpus/templates/content/shared/api.ts +2 -0
  11. package/corpus/templates/content/shared/content-source.ts +14 -0
  12. package/corpus/templates/content/shared/document-text-edits.ts +39 -0
  13. package/corpus/templates/content/shared/nfm.ts +347 -1
  14. package/corpus/templates/slides/server/plugins/agent-chat.ts +2 -0
  15. package/dist/agent/engine/ai-sdk-engine.js +14 -4
  16. package/dist/agent/engine/anthropic-engine.js +6 -4
  17. package/dist/agent/engine/builder-engine.js +11 -4
  18. package/dist/agent/engine/tool-name.d.ts +9 -0
  19. package/dist/agent/engine/tool-name.js +56 -0
  20. package/dist/agent/engine/translate-ai-sdk.d.ts +5 -3
  21. package/dist/agent/engine/translate-ai-sdk.js +16 -13
  22. package/dist/agent/engine/translate-anthropic.d.ts +8 -6
  23. package/dist/agent/engine/translate-anthropic.js +21 -22
  24. package/dist/agent/production-agent.js +23 -5
  25. package/dist/collab/routes.d.ts +1 -1
  26. package/dist/collab/struct-routes.d.ts +1 -1
  27. package/dist/extensions/fetch-tool.js +73 -5
  28. package/dist/extensions/proxy-security.d.ts +5 -0
  29. package/dist/extensions/proxy-security.js +21 -13
  30. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  31. package/dist/notifications/routes.d.ts +3 -3
  32. package/dist/server/agent-chat/context-tools.js +5 -0
  33. package/dist/server/credential-provider.js +47 -10
  34. package/dist/server/realtime-token.d.ts +1 -1
  35. package/docs/content/webmcp.mdx +5 -5
  36. package/package.json +1 -1
@@ -25,7 +25,9 @@
25
25
  * lines (Notion strips them). Intentional blank blocks are `<empty-block/>`.
26
26
  * - Block attributes as a trailing `{toggle="true" color="red"}` list.
27
27
  * - Tables, toggles, callouts, columns, synced blocks, media, mentions use the
28
- * HTML-ish tags from the spec. Tables are `<table>` HTML, never pipe tables.
28
+ * HTML-ish tags from the spec. Canonical output uses `<table>` HTML; input
29
+ * may also use an unaligned GFM pipe table, which is promoted to that typed
30
+ * table grammar instead of flattening each row into a paragraph.
29
31
  * - Inline text backslash-escapes the spec's special characters outside code.
30
32
  *
31
33
  * Registry blocks (the dev-doc / OpenAPI library shared with plan) are encoded
@@ -61,6 +63,20 @@ export interface PMDoc {
61
63
  content: PMNode[];
62
64
  }
63
65
 
66
+ export type NfmFidelityStatus =
67
+ | "preserved"
68
+ | "transformed"
69
+ | "unresolved"
70
+ | "failed";
71
+
72
+ export interface NfmFidelityReport {
73
+ status: NfmFidelityStatus;
74
+ normalizedChanged: boolean;
75
+ conversions: Array<{ kind: string; count: number }>;
76
+ unresolved: Array<{ kind: string; count: number }>;
77
+ error?: string;
78
+ }
79
+
64
80
  // ── Colors (from the NFM spec) ──────────────────────────────────────
65
81
  const BASE_COLORS = [
66
82
  "gray",
@@ -1315,6 +1331,13 @@ function parseBlockSequence(
1315
1331
  const dedent = raw.slice(ind);
1316
1332
  const rel = ind - baseIndent;
1317
1333
 
1334
+ const pipeTable = parseGfmPipeTable(lines, i, ind, rel);
1335
+ if (pipeTable) {
1336
+ out.push(pipeTable.nodes[0]);
1337
+ i = pipeTable.end;
1338
+ continue;
1339
+ }
1340
+
1318
1341
  // Lists group consecutive items.
1319
1342
  const listKind = listKindOf(dedent);
1320
1343
  if (listKind) {
@@ -1332,6 +1355,146 @@ function parseBlockSequence(
1332
1355
  return { nodes: out, end: i };
1333
1356
  }
1334
1357
 
1358
+ function endsWithUnescapedPipe(value: string): boolean {
1359
+ if (!value.endsWith("|")) return false;
1360
+ let backslashes = 0;
1361
+ for (let i = value.length - 2; i >= 0 && value[i] === "\\"; i--) {
1362
+ backslashes++;
1363
+ }
1364
+ return backslashes % 2 === 0;
1365
+ }
1366
+
1367
+ function splitGfmPipeRow(line: string): string[] | null {
1368
+ const value = line.trim();
1369
+ if (!value) return null;
1370
+
1371
+ const cells: string[] = [];
1372
+ let cell = "";
1373
+ let codeFenceLength = 0;
1374
+ let sawSeparator = false;
1375
+
1376
+ for (let i = 0; i < value.length; i++) {
1377
+ const char = value[i];
1378
+ if (char === "\\" && i + 1 < value.length) {
1379
+ cell += char + value[++i];
1380
+ continue;
1381
+ }
1382
+ if (char === "`") {
1383
+ let runLength = 1;
1384
+ while (value[i + runLength] === "`") runLength++;
1385
+ if (codeFenceLength === 0) codeFenceLength = runLength;
1386
+ else if (codeFenceLength === runLength) codeFenceLength = 0;
1387
+ cell += "`".repeat(runLength);
1388
+ i += runLength - 1;
1389
+ continue;
1390
+ }
1391
+ if (char === "|" && codeFenceLength === 0) {
1392
+ cells.push(cell.trim());
1393
+ cell = "";
1394
+ sawSeparator = true;
1395
+ continue;
1396
+ }
1397
+ cell += char;
1398
+ }
1399
+ cells.push(cell.trim());
1400
+
1401
+ if (!sawSeparator) return null;
1402
+ if (value.startsWith("|")) cells.shift();
1403
+ if (endsWithUnescapedPipe(value)) cells.pop();
1404
+ return cells;
1405
+ }
1406
+
1407
+ function isGfmDelimiterCell(value: string): boolean {
1408
+ return /^-{3,}$/.test(value.trim());
1409
+ }
1410
+
1411
+ function isAlignedGfmDelimiterCell(value: string): boolean {
1412
+ return /^:?-{3,}:?$/.test(value.trim()) && value.includes(":");
1413
+ }
1414
+
1415
+ function parseGfmPipeTable(
1416
+ lines: string[],
1417
+ start: number,
1418
+ indent: number,
1419
+ rel: number,
1420
+ ): ParseResult | null {
1421
+ if (start + 1 >= lines.length) return null;
1422
+ if (leadingTabs(lines[start + 1]) !== indent) return null;
1423
+
1424
+ const header = splitGfmPipeRow(lines[start].slice(indent));
1425
+ const delimiter = splitGfmPipeRow(lines[start + 1].slice(indent));
1426
+ if (
1427
+ !header ||
1428
+ !delimiter ||
1429
+ header.length === 0 ||
1430
+ header.length !== delimiter.length ||
1431
+ !delimiter.every(
1432
+ (cell) => isGfmDelimiterCell(cell) || isAlignedGfmDelimiterCell(cell),
1433
+ )
1434
+ ) {
1435
+ return null;
1436
+ }
1437
+
1438
+ const rows = [header];
1439
+ let end = start + 2;
1440
+ let ragged = false;
1441
+ while (end < lines.length) {
1442
+ if (lines[end].trim() === "" || leadingTabs(lines[end]) !== indent) break;
1443
+ const row = splitGfmPipeRow(lines[end].slice(indent));
1444
+ if (!row) break;
1445
+ if (row.length !== header.length) ragged = true;
1446
+ rows.push(row);
1447
+ end++;
1448
+ }
1449
+
1450
+ const hasAlignment = delimiter.some(isAlignedGfmDelimiterCell);
1451
+ if (hasAlignment || ragged) {
1452
+ // Content's table grammar cannot represent column alignment or a ragged
1453
+ // row without losing meaning. Keep the entire source construct together
1454
+ // as one inspectable raw atom rather than escaping or partially promoting
1455
+ // its lines into plausible-looking paragraphs.
1456
+ return {
1457
+ nodes: [
1458
+ {
1459
+ type: "notionBlockAtom",
1460
+ attrs: {
1461
+ tagName: "unknown",
1462
+ attrsJson: JSON.stringify({ sourceFormat: "gfm-table" }),
1463
+ label: "Unresolved GFM table",
1464
+ __raw: lines
1465
+ .slice(start, end)
1466
+ .map((line) => line.slice(indent))
1467
+ .join("\n"),
1468
+ ...(rel > 0 ? { indent: rel } : {}),
1469
+ },
1470
+ },
1471
+ ],
1472
+ end,
1473
+ };
1474
+ }
1475
+
1476
+ const tableRows = rows.map((row, rowIndex) => ({
1477
+ type: "tableRow",
1478
+ attrs: { color: null },
1479
+ content: row.map((cell) => ({
1480
+ type: rowIndex === 0 ? "tableHeader" : "tableCell",
1481
+ attrs: { color: null },
1482
+ content: [{ type: "paragraph", content: parseInline(cell) }],
1483
+ })),
1484
+ }));
1485
+ const attrs: Record<string, unknown> = {
1486
+ headerRow: true,
1487
+ headerColumn: false,
1488
+ fitPageWidth: false,
1489
+ colMeta: null,
1490
+ };
1491
+ if (rel > 0) attrs.indent = rel;
1492
+ return {
1493
+ nodes: [{ type: "table", attrs, content: tableRows }],
1494
+ end,
1495
+ };
1496
+ }
1497
+
1335
1498
  type ListKind = "bullet" | "ordered" | "task";
1336
1499
  function listKindOf(dedent: string): ListKind | null {
1337
1500
  if (/^- \[[ xX]\]\s/.test(dedent)) return "task";
@@ -2263,6 +2426,189 @@ export function canonicalizeNfm(nfm: string | null | undefined): string {
2263
2426
  }
2264
2427
  }
2265
2428
 
2429
+ function countLocalMdxNodes(
2430
+ node: PMNode,
2431
+ predicate: (node: PMNode) => boolean,
2432
+ ): number {
2433
+ let count = node.type === "localMdxComponent" && predicate(node) ? 1 : 0;
2434
+ for (const child of node.content ?? []) {
2435
+ count += countLocalMdxNodes(child, predicate);
2436
+ }
2437
+ return count;
2438
+ }
2439
+
2440
+ function countGfmPipeTables(
2441
+ source: string,
2442
+ delimiterPredicate: (cells: string[]) => boolean,
2443
+ ): number {
2444
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
2445
+ const fencedCodeLines = fencedCodeLineMask(lines);
2446
+ let count = 0;
2447
+ for (let i = 0; i + 1 < lines.length; i++) {
2448
+ if (fencedCodeLines[i] || fencedCodeLines[i + 1]) continue;
2449
+ const indent = leadingTabs(lines[i]);
2450
+ if (leadingTabs(lines[i + 1]) !== indent) continue;
2451
+ const header = splitGfmPipeRow(lines[i].slice(indent));
2452
+ const delimiter = splitGfmPipeRow(lines[i + 1].slice(indent));
2453
+ if (
2454
+ header &&
2455
+ delimiter &&
2456
+ header.length > 0 &&
2457
+ header.length === delimiter.length &&
2458
+ delimiterPredicate(delimiter)
2459
+ ) {
2460
+ count++;
2461
+ i++;
2462
+ }
2463
+ }
2464
+ return count;
2465
+ }
2466
+
2467
+ function countRaggedGfmPipeTables(source: string): number {
2468
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
2469
+ const fencedCodeLines = fencedCodeLineMask(lines);
2470
+ let count = 0;
2471
+ for (let i = 0; i + 2 < lines.length; i++) {
2472
+ if (fencedCodeLines[i] || fencedCodeLines[i + 1]) continue;
2473
+ const indent = leadingTabs(lines[i]);
2474
+ if (leadingTabs(lines[i + 1]) !== indent) continue;
2475
+ const header = splitGfmPipeRow(lines[i].slice(indent));
2476
+ const delimiter = splitGfmPipeRow(lines[i + 1].slice(indent));
2477
+ if (
2478
+ !header ||
2479
+ !delimiter ||
2480
+ header.length === 0 ||
2481
+ header.length !== delimiter.length ||
2482
+ !delimiter.every(isGfmDelimiterCell)
2483
+ ) {
2484
+ continue;
2485
+ }
2486
+ for (let rowIndex = i + 2; rowIndex < lines.length; rowIndex++) {
2487
+ if (fencedCodeLines[rowIndex]) break;
2488
+ if (
2489
+ lines[rowIndex].trim() === "" ||
2490
+ leadingTabs(lines[rowIndex]) !== indent
2491
+ ) {
2492
+ break;
2493
+ }
2494
+ const row = splitGfmPipeRow(lines[rowIndex].slice(indent));
2495
+ if (!row) break;
2496
+ if (row.length !== header.length) {
2497
+ count++;
2498
+ break;
2499
+ }
2500
+ }
2501
+ i++;
2502
+ }
2503
+ return count;
2504
+ }
2505
+
2506
+ function fencedCodeLineMask(lines: string[]): boolean[] {
2507
+ const mask = lines.map(() => false);
2508
+ let fence: { indent: number; length: number } | undefined;
2509
+ for (let index = 0; index < lines.length; index++) {
2510
+ const indent = leadingTabs(lines[index]);
2511
+ const dedented = lines[index].slice(indent);
2512
+ if (fence) {
2513
+ mask[index] = true;
2514
+ const close = dedented.match(/^(`{3,})\s*$/);
2515
+ if (indent >= fence.indent && close && close[1].length >= fence.length) {
2516
+ fence = undefined;
2517
+ }
2518
+ continue;
2519
+ }
2520
+ const open = dedented.match(/^(`{3,})(.*)$/);
2521
+ if (open) {
2522
+ mask[index] = true;
2523
+ fence = { indent, length: open[1].length };
2524
+ }
2525
+ }
2526
+ return mask;
2527
+ }
2528
+
2529
+ /**
2530
+ * Describe what the shared codec did without making callers infer fidelity
2531
+ * from a clean-looking string. The document body remains the source of truth;
2532
+ * this report is intentionally compact enough for import and agent receipts.
2533
+ */
2534
+ export function inspectNfmFidelity(
2535
+ nfm: string | null | undefined,
2536
+ ): NfmFidelityReport {
2537
+ const source = nfm ?? "";
2538
+ try {
2539
+ const document = nfmToDoc(source);
2540
+ const normalized = canonicalizeNfm(source);
2541
+ const raggedPipeTableCount = countRaggedGfmPipeTables(source);
2542
+ const pipeTableCount = Math.max(
2543
+ 0,
2544
+ countGfmPipeTables(source, (cells) => cells.every(isGfmDelimiterCell)) -
2545
+ raggedPipeTableCount,
2546
+ );
2547
+ const alignedPipeTableCount = countGfmPipeTables(
2548
+ source,
2549
+ (cells) =>
2550
+ cells.every(
2551
+ (cell) => isGfmDelimiterCell(cell) || isAlignedGfmDelimiterCell(cell),
2552
+ ) && cells.some(isAlignedGfmDelimiterCell),
2553
+ );
2554
+ const unsupportedMdxCount = countLocalMdxNodes(
2555
+ document,
2556
+ (node) => node.attrs?.unsupportedProps === true,
2557
+ );
2558
+
2559
+ const conversions: NfmFidelityReport["conversions"] = [];
2560
+ if (pipeTableCount > 0) {
2561
+ conversions.push({
2562
+ kind: "gfm-pipe-table-to-content-table",
2563
+ count: pipeTableCount,
2564
+ });
2565
+ }
2566
+ if (normalized !== source && conversions.length === 0) {
2567
+ conversions.push({ kind: "canonicalized-nfm", count: 1 });
2568
+ }
2569
+
2570
+ const unresolved: NfmFidelityReport["unresolved"] = [];
2571
+ if (alignedPipeTableCount > 0) {
2572
+ unresolved.push({
2573
+ kind: "gfm-table-alignment-not-representable",
2574
+ count: alignedPipeTableCount,
2575
+ });
2576
+ }
2577
+ if (raggedPipeTableCount > 0) {
2578
+ unresolved.push({
2579
+ kind: "gfm-table-ragged-rows-preserved-as-raw-source",
2580
+ count: raggedPipeTableCount,
2581
+ });
2582
+ }
2583
+ if (unsupportedMdxCount > 0) {
2584
+ unresolved.push({
2585
+ kind: "mdx-component-props-preserved-as-raw-source",
2586
+ count: unsupportedMdxCount,
2587
+ });
2588
+ }
2589
+
2590
+ return {
2591
+ status:
2592
+ unresolved.length > 0
2593
+ ? "unresolved"
2594
+ : conversions.length > 0 || normalized !== source
2595
+ ? "transformed"
2596
+ : "preserved",
2597
+ normalizedChanged: normalized !== source,
2598
+ conversions,
2599
+ unresolved,
2600
+ };
2601
+ } catch (error) {
2602
+ return {
2603
+ status: "failed",
2604
+ normalizedChanged: false,
2605
+ conversions: [],
2606
+ unresolved: [],
2607
+ error: error instanceof Error ? error.message : "Unreadable content",
2608
+ };
2609
+ }
2610
+ }
2611
+
2266
2612
  export function collapseExactRepeatedNfm(
2267
2613
  nfm: string,
2268
2614
  options: { requiredText: string },
@@ -66,6 +66,8 @@ export default createAgentChatPlugin({
66
66
  prepareRequest: prepareSlidesChatAttachments,
67
67
  systemPrompt: `You are an AI deck assistant. You create, edit, import, export, style, share, and navigate decks through actions and shared application state. For a newly created presentation, use create-deck with slides: [] only when you are creating the deck yourself, then add-slide sequentially with full rendered HTML. The legacy generate-slides-ai action returns Markdown drafts and is not part of the persisted presentation workflow. When speaker notes are requested, keep presenter-only text in each slide's notes field rather than the slide HTML, and preserve notes during source-preserving edits.
68
68
 
69
+ When a request includes a public URL as source material, fetch it with web-request before authoring. Inspect the returned page content, links, and agent-readable metadata, then follow any context, transcript, visual/frame, or asset URLs it exposes. Image responses are visual evidence for the deck and should be inspected when available; do not claim to have reviewed visuals that could not be fetched.
70
+
69
71
  When the user asks to improve, beautify, restyle, or make an uploaded/existing deck on-brand, treat it as an in-place source-preserving edit unless the user explicitly asks to rewrite the story or change slide count. First call view-screen when the active deck is unclear, then get-deck with compact=true for deck orientation. If you need slide markup, call get-deck with compact=false explicitly and only when the edit requires the full HTML. If get-deck.sourceImport exists, preserve its slide count, order, IDs, factual copy, notes, images, charts, tables, diagrams, freeform objects, and source aspect ratio. For a deck-wide restyle, use one patch-deck call with requireAllSourceSlides=true and one patch-slide operation with fields.content for every source slide ID; the action rejects partial coverage. Do not split a full-deck restyle into arbitrary batches or use one-by-one update-slide calls - reserve update-slide for targeted one-slide edits. After the patch succeeds, verify with get-deck using compact=true so the verification does not retransmit every slide's HTML. Do not claim a partial or initial pass. Do not use add-slide, delete, reorder, or replace source imagery with generic cards for this workflow. If sourceImport.fidelity is partial or imagesSkipped is nonzero, stop and report the exact fidelity warning instead of claiming a reliable improvement.
70
72
  For a targeted one-slide edit, prefer get-deck with the stable slideId from view-screen or compact get-deck. It returns only that slide's full HTML by default, so do not load the full deck when one slide is enough. For code-style edits, request compact=false and format=true, then call update-slide with that slide's contentHash as baseContentHash and an ordered edits list. Use exact replace, insert-before/after, replace-between, or regex-replace edits; include expectedMatches for ambiguous markers. The action applies the whole list atomically under the deck lock, so a failed required match writes nothing. Set format=true on update-slide when readable line breaks should be persisted. Use fullContent only for an intentional full rewrite. Pass compact=true when you only need a lightweight check of that slide.
71
73
 
@@ -18,6 +18,7 @@ import { LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, }
18
18
  import { classifyProviderError, describeErrorWithCauses, } from "./error-detail.js";
19
19
  import { createFirstEventAbortController } from "./first-event-timeout.js";
20
20
  import { clampThinkingBudgetTokens, resolveMaxOutputTokensForEngine, } from "./output-tokens.js";
21
+ import { createProviderToolNameMap } from "./tool-name.js";
21
22
  import { engineToolsToAISDK, engineMessagesToAISDK, aiSdkPartToEngineEvents, aiSdkStepToAssistantContent, } from "./translate-ai-sdk.js";
22
23
  import { createStreamedToolInputState, finalizeStreamedToolInputs, observeStreamedToolInput, } from "./translate-anthropic.js";
23
24
  // ---------------------------------------------------------------------------
@@ -235,14 +236,16 @@ class AISDKEngine {
235
236
  };
236
237
  return;
237
238
  }
239
+ const toolNameMap = createProviderToolNameMap(opts.tools, opts.messages);
238
240
  const aiSdkTools = opts.tools.length > 0
239
- ? engineToolsToAISDK(opts.tools, jsonSchema)
241
+ ? engineToolsToAISDK(opts.tools, jsonSchema, toolNameMap)
240
242
  : undefined;
241
243
  const messages = engineMessagesToAISDK(opts.messages, {
242
244
  // Vision-capable provider translators (anthropic/openai/google/
243
245
  // openrouter) map image parts to native blocks; the rest stringify
244
246
  // tool-result content arrays, so images degrade to their text notes.
245
247
  toolResultImages: this.capabilities.vision,
248
+ toolNameMap,
246
249
  });
247
250
  // Resolved once so both `maxOutputTokens` (below, in the streamText call)
248
251
  // and the thinking-budget headroom clamp agree on the same ceiling.
@@ -381,7 +384,7 @@ class AISDKEngine {
381
384
  : {}),
382
385
  abortSignal: firstEventAbort.signal,
383
386
  onStepFinish: (step) => {
384
- assistantContent = aiSdkStepToAssistantContent(step);
387
+ assistantContent = aiSdkStepToAssistantContent(step, toolNameMap);
385
388
  },
386
389
  ...(Object.keys(providerOpts).length > 0
387
390
  ? { providerOptions: providerOpts }
@@ -402,7 +405,7 @@ class AISDKEngine {
402
405
  sawFirstEvent = true;
403
406
  firstEventAbort.markFirstEvent();
404
407
  }
405
- for (const event of aiSdkPartToEngineEvents(part)) {
408
+ for (const event of aiSdkPartToEngineEvents(part, toolNameMap)) {
406
409
  observeStreamedToolInput(toolInputs, event);
407
410
  if (event.type === "stop" &&
408
411
  event.reason === "error" &&
@@ -453,7 +456,14 @@ class AISDKEngine {
453
456
  yield recovered;
454
457
  }
455
458
  yield { type: "assistant-content", parts: assistantContent };
456
- if (!credentialFailureRecorded) {
459
+ // Clearing the marker asserts "this credential demonstrably works", so
460
+ // only a turn that actually completed may do it. A turn that ended in
461
+ // error proves nothing about the credential, and this ran on every one:
462
+ // a single unrelated 500 re-admitted a credential a 401 had just pinned,
463
+ // so the next person's first prompt spent itself rediscovering the same
464
+ // rejection. `credentialFailureRecorded` only covers the 401 path.
465
+ const stoppedWithError = bufferedStop?.type === "stop" && bufferedStop.reason === "error";
466
+ if (!credentialFailureRecorded && !stoppedWithError) {
457
467
  await clearProviderCredentialAuthFailure({
458
468
  key: PROVIDER_ENV_VARS[this.provider][0],
459
469
  value: this.apiKey,
@@ -15,6 +15,7 @@ import { describeErrorWithCauses } from "./error-detail.js";
15
15
  import { createFirstEventAbortController } from "./first-event-timeout.js";
16
16
  import { clampThinkingBudgetTokens, resolveMaxOutputTokensForEngine, } from "./output-tokens.js";
17
17
  import { splitSystemPromptForCache, stablePrefixCacheControl, } from "./prompt-cache.js";
18
+ import { createProviderToolNameMap } from "./tool-name.js";
18
19
  import { engineToolsToAnthropic, engineMessagesToAnthropic, anthropicContentToEngine, anthropicChunkToEngineEvents, createAnthropicChunkStreamState, createStreamedToolInputState, finalizeStreamedToolInputs, observeStreamedToolInput, } from "./translate-anthropic.js";
19
20
  export const ANTHROPIC_CAPABILITIES = {
20
21
  thinking: true,
@@ -41,8 +42,9 @@ class AnthropicEngine {
41
42
  // MAX_RETRIES times with backoff. Leaving the SDK on its default (2)
42
43
  // multiplies the two layers into ~12 HTTP requests per failed run.
43
44
  const client = new Anthropic({ apiKey: this.apiKey, maxRetries: 1 });
44
- const tools = engineToolsToAnthropic(opts.tools);
45
- const messages = engineMessagesToAnthropic(opts.messages);
45
+ const toolNameMap = createProviderToolNameMap(opts.tools, opts.messages);
46
+ const tools = engineToolsToAnthropic(opts.tools, toolNameMap);
47
+ const messages = engineMessagesToAnthropic(opts.messages, toolNameMap);
46
48
  const anthropicOpts = opts.providerOptions?.anthropic;
47
49
  // Resolved once so both max_tokens and the thinking-budget headroom
48
50
  // clamp below agree on the same ceiling.
@@ -170,14 +172,14 @@ class AnthropicEngine {
170
172
  // The SDK's SSE parsing already drops `ping` keepalives before they
171
173
  // reach this loop, so any chunk here is real provider progress.
172
174
  firstEventAbort.markFirstEvent();
173
- const events = anthropicChunkToEngineEvents(chunk, chunkState);
175
+ const events = anthropicChunkToEngineEvents(chunk, chunkState, toolNameMap);
174
176
  for (const event of events) {
175
177
  observeStreamedToolInput(toolInputs, event);
176
178
  yield event;
177
179
  }
178
180
  }
179
181
  const finalMessage = await apiStream.finalMessage();
180
- const assistantContent = anthropicContentToEngine(finalMessage.content);
182
+ const assistantContent = anthropicContentToEngine(finalMessage.content, toolNameMap);
181
183
  // The final content can carry the same tool call with an empty input
182
184
  // even though the stream already delivered complete argument deltas.
183
185
  // Reconcile that copy before marking its id delivered.
@@ -29,6 +29,7 @@ import { classifyTerminalErrorCode, describeErrorWithCauses, isBuilderGatewayInt
29
29
  import { FIRST_STREAM_EVENT_TIMEOUT_MS } from "./first-event-timeout.js";
30
30
  import { resolveMaxOutputTokensForEngine } from "./output-tokens.js";
31
31
  import { splitSystemPromptForCache, stablePrefixCacheControl, } from "./prompt-cache.js";
32
+ import { createProviderToolNameMap, toEngineToolName, } from "./tool-name.js";
32
33
  import { createStreamedToolInputState, engineMessagesToBuilderGatewayAnthropic, engineToolsToAnthropic, finalizeStreamedToolInputs, observeStreamedToolInput, } from "./translate-anthropic.js";
33
34
  export const BUILDER_CAPABILITIES = {
34
35
  thinking: true,
@@ -173,8 +174,9 @@ class BuilderEngine {
173
174
  const model = requestedModel.length === 0 || requestedModel === "auto"
174
175
  ? BUILDER_DEFAULT_MODEL
175
176
  : requestedModel;
176
- const messages = engineMessagesToBuilderGatewayAnthropic(opts.messages);
177
- const tools = engineToolsToAnthropic(opts.tools);
177
+ const toolNameMap = createProviderToolNameMap(opts.tools, opts.messages);
178
+ const messages = engineMessagesToBuilderGatewayAnthropic(opts.messages, toolNameMap);
179
+ const tools = engineToolsToAnthropic(opts.tools, toolNameMap);
178
180
  const thinkingBudget = opts.providerOptions?.anthropic?.thinking?.budgetTokens;
179
181
  const reasoningEffort = normalizeReasoningEffortForModel(model, opts.reasoningEffort ??
180
182
  (typeof thinkingBudget === "number"
@@ -383,6 +385,7 @@ class BuilderEngine {
383
385
  return;
384
386
  }
385
387
  yield* parseJsonlStream(reader, model, {
388
+ toolNameMap,
386
389
  creditsLane,
387
390
  abortSignal: gatewayAbort.signal,
388
391
  didGatewayTimeout: gatewayAbort.didTimeout,
@@ -675,7 +678,9 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
675
678
  const delta = {
676
679
  type: "tool-input-delta",
677
680
  id: event.id,
678
- name: event.name,
681
+ name: typeof event.name === "string"
682
+ ? toEngineToolName(event.name, captureContext.toolNameMap)
683
+ : event.name,
679
684
  text: typeof event.argsTextDelta === "string"
680
685
  ? event.argsTextDelta
681
686
  : typeof event.delta === "string"
@@ -694,7 +699,9 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
694
699
  const call = {
695
700
  type: "tool-call",
696
701
  id: event.id,
697
- name: event.name,
702
+ name: typeof event.name === "string"
703
+ ? toEngineToolName(event.name, captureContext.toolNameMap)
704
+ : event.name,
698
705
  input: event.input,
699
706
  };
700
707
  parts.push(call);
@@ -0,0 +1,9 @@
1
+ import type { EngineMessage, EngineTool } from "./types.js";
2
+ export declare const PROVIDER_TOOL_NAME_MAX_LENGTH = 64;
3
+ export interface ProviderToolNameMap {
4
+ toEngine: ReadonlyMap<string, string>;
5
+ toProvider: ReadonlyMap<string, string>;
6
+ }
7
+ export declare function createProviderToolNameMap(tools: readonly EngineTool[], messages?: readonly EngineMessage[]): ProviderToolNameMap;
8
+ export declare function toProviderToolName(name: string, map?: ProviderToolNameMap): string;
9
+ export declare function toEngineToolName(name: string, map?: ProviderToolNameMap): string;
@@ -0,0 +1,56 @@
1
+ export const PROVIDER_TOOL_NAME_MAX_LENGTH = 64;
2
+ function hashToolName(value, seed) {
3
+ let hash = seed;
4
+ for (let index = 0; index < value.length; index += 1) {
5
+ hash ^= value.charCodeAt(index);
6
+ hash = Math.imul(hash, 16777619);
7
+ }
8
+ return (hash >>> 0).toString(36);
9
+ }
10
+ function providerToolAlias(name) {
11
+ return `tool_${hashToolName(name, 2166136261)}_${hashToolName(name, 16777619)}`;
12
+ }
13
+ function toolNamesFromMessages(messages) {
14
+ return messages.flatMap((message) => message.content.flatMap((part) => {
15
+ if (part.type === "tool-call")
16
+ return [part.name];
17
+ if (part.type === "tool-result")
18
+ return [part.toolName];
19
+ return [];
20
+ }));
21
+ }
22
+ export function createProviderToolNameMap(tools, messages = []) {
23
+ const names = new Set([
24
+ ...tools.map((tool) => tool.name),
25
+ ...toolNamesFromMessages(messages),
26
+ ]);
27
+ const usedProviderNames = new Set([...names].filter((name) => name.length <= PROVIDER_TOOL_NAME_MAX_LENGTH));
28
+ const toProvider = new Map();
29
+ const toEngine = new Map();
30
+ for (const name of names) {
31
+ if (name.length <= PROVIDER_TOOL_NAME_MAX_LENGTH) {
32
+ toProvider.set(name, name);
33
+ toEngine.set(name, name);
34
+ continue;
35
+ }
36
+ const base = providerToolAlias(name);
37
+ let alias = base;
38
+ let suffix = 1;
39
+ while (usedProviderNames.has(alias)) {
40
+ alias = `${base}_${suffix}`;
41
+ suffix += 1;
42
+ }
43
+ usedProviderNames.add(alias);
44
+ toProvider.set(name, alias);
45
+ toEngine.set(alias, name);
46
+ }
47
+ return { toEngine, toProvider };
48
+ }
49
+ export function toProviderToolName(name, map) {
50
+ if (name.length <= PROVIDER_TOOL_NAME_MAX_LENGTH)
51
+ return name;
52
+ return map?.toProvider.get(name) ?? providerToolAlias(name);
53
+ }
54
+ export function toEngineToolName(name, map) {
55
+ return map?.toEngine.get(name) ?? name;
56
+ }
@@ -6,6 +6,7 @@
6
6
  * These helpers convert in both directions against the v6 `TextStreamPart` and
7
7
  * `ModelMessage` shapes.
8
8
  */
9
+ import { type ProviderToolNameMap } from "./tool-name.js";
9
10
  import type { EngineTool, EngineMessage, EngineContentPart, EngineEvent } from "./types.js";
10
11
  /**
11
12
  * Convert EngineTool[] into the record shape that AI SDK's `streamText` expects
@@ -15,7 +16,7 @@ import type { EngineTool, EngineMessage, EngineContentPart, EngineEvent } from "
15
16
  * schema is wrapped in the SDK's runtime validator; fall back to the raw JSON
16
17
  * Schema object otherwise (mostly for unit tests that don't import `ai`).
17
18
  */
18
- export declare function engineToolsToAISDK(tools: EngineTool[], jsonSchema?: (schema: Record<string, unknown>) => unknown): Record<string, any>;
19
+ export declare function engineToolsToAISDK(tools: EngineTool[], jsonSchema?: (schema: Record<string, unknown>) => unknown, toolNameMap?: ProviderToolNameMap): Record<string, any>;
19
20
  /**
20
21
  * Convert a single EngineMessage into **one or more** AI SDK ModelMessages.
21
22
  *
@@ -36,6 +37,7 @@ export interface EngineToAISDKOptions {
36
37
  * string keep the model informed of what it cannot see.
37
38
  */
38
39
  toolResultImages?: boolean;
40
+ toolNameMap?: ProviderToolNameMap;
39
41
  }
40
42
  export declare function engineMessageToAISDK(msg: EngineMessage, opts?: EngineToAISDKOptions): any[];
41
43
  export declare function engineMessagesToAISDK(messages: EngineMessage[], opts?: EngineToAISDKOptions): any[];
@@ -48,9 +50,9 @@ export declare function engineMessagesToAISDK(messages: EngineMessage[], opts?:
48
50
  * We absorb text/reasoning boundaries, forward text/reasoning/tool-input
49
51
  * deltas, and keep the terminal `tool-call`, `finish-step`, and `finish` parts.
50
52
  */
51
- export declare function aiSdkPartToEngineEvents(part: any): EngineEvent[];
53
+ export declare function aiSdkPartToEngineEvents(part: any, toolNameMap?: ProviderToolNameMap): EngineEvent[];
52
54
  /**
53
55
  * Reconstruct the assistant message content from an AI SDK v6 `StepResult`.
54
56
  * `step.content` is the canonical structured form — iterate it.
55
57
  */
56
- export declare function aiSdkStepToAssistantContent(step: any): EngineContentPart[];
58
+ export declare function aiSdkStepToAssistantContent(step: any, toolNameMap?: ProviderToolNameMap): EngineContentPart[];