@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/clips/server/plugins/agent-chat.ts +3 -0
- package/corpus/templates/content/actions/_linked-local-document-edit.ts +167 -0
- package/corpus/templates/content/actions/edit-document.ts +181 -61
- package/corpus/templates/content/actions/import-content-source.ts +33 -1
- package/corpus/templates/content/actions/update-document.ts +2 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +68 -0
- package/corpus/templates/content/app/components/editor/LinkedLocalDocumentAgentBridge.tsx +245 -0
- package/corpus/templates/content/app/lib/local-content-source-files.ts +96 -9
- package/corpus/templates/content/shared/api.ts +2 -0
- package/corpus/templates/content/shared/content-source.ts +14 -0
- package/corpus/templates/content/shared/document-text-edits.ts +39 -0
- package/corpus/templates/content/shared/nfm.ts +347 -1
- package/corpus/templates/slides/server/plugins/agent-chat.ts +2 -0
- package/dist/agent/engine/ai-sdk-engine.js +14 -4
- package/dist/agent/engine/anthropic-engine.js +6 -4
- package/dist/agent/engine/builder-engine.js +11 -4
- package/dist/agent/engine/tool-name.d.ts +9 -0
- package/dist/agent/engine/tool-name.js +56 -0
- package/dist/agent/engine/translate-ai-sdk.d.ts +5 -3
- package/dist/agent/engine/translate-ai-sdk.js +16 -13
- package/dist/agent/engine/translate-anthropic.d.ts +8 -6
- package/dist/agent/engine/translate-anthropic.js +21 -22
- package/dist/agent/production-agent.js +23 -5
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/fetch-tool.js +73 -5
- package/dist/extensions/proxy-security.d.ts +5 -0
- package/dist/extensions/proxy-security.js +21 -13
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/server/agent-chat/context-tools.js +5 -0
- package/dist/server/credential-provider.js +47 -10
- package/dist/server/realtime-token.d.ts +1 -1
- package/docs/content/webmcp.mdx +5 -5
- 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.
|
|
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
|
-
|
|
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
|
|
45
|
-
const
|
|
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
|
|
177
|
-
const
|
|
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[];
|