@superdoc/sdk 2.13.0-next.8 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/actions.cjs +118 -117
- package/dist/agent/actions.d.ts +19 -0
- package/dist/agent/actions.js +116 -117
- package/dist/agent/catalog.cjs +52 -8
- package/dist/agent/catalog.d.ts +54 -1
- package/dist/agent/catalog.js +55 -8
- package/dist/embedded-tools.generated.cjs +5 -5
- package/dist/embedded-tools.generated.js +5 -5
- package/dist/generated/client.d.ts +3762 -969
- package/dist/generated/contract.cjs +1665 -2
- package/dist/generated/contract.js +1666 -2
- package/dist/index.cjs +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/presets/core.cjs +77 -15
- package/dist/presets/core.js +79 -17
- package/dist/presets.d.ts +23 -0
- package/dist/runtime/host.cjs +24 -15
- package/dist/runtime/host.d.ts +3 -1
- package/dist/runtime/host.js +24 -15
- package/dist/runtime/sdk-version.generated.cjs +1 -1
- package/dist/runtime/sdk-version.generated.d.ts +1 -1
- package/dist/runtime/sdk-version.generated.js +1 -1
- package/dist/tools.cjs +34 -8
- package/dist/tools.d.ts +28 -3
- package/dist/tools.js +34 -8
- package/package.json +7 -7
- package/tools/__pycache__/__init__.cpython-311.pyc +0 -0
- package/tools/__pycache__/intent_dispatch_generated.cpython-311.pyc +0 -0
- package/tools/catalog.json +3517 -427
- package/tools/tools-policy.json +1 -1
- package/tools/tools.anthropic.json +3371 -281
- package/tools/tools.generic.json +4089 -999
- package/tools/tools.openai.json +4072 -982
- package/tools/tools.vercel.json +4072 -982
package/dist/agent/actions.js
CHANGED
|
@@ -62,7 +62,7 @@ export const ACTION_HINTS = {
|
|
|
62
62
|
delete_blocks: 'selectors[] (each resolving to ONE inspected block: list item, paragraph or heading), changeMode? — THE way to DELETE a whole LIST ITEM, paragraph or heading, bullet/number included. Use for "remove the first item under Article II" / "delete that clause". delete_text only strikes the text and leaves an empty numbered item behind. Deletes several blocks in ONE call; use delete_table for a whole table and delete_text to clear a table cell (a cell must keep a paragraph)',
|
|
63
63
|
append_list: 'items[], kind?: ordered|bullet, headingText?, headingLevel?, placement? {at:"after"|"before",selector} builds the list at that block instead of document end',
|
|
64
64
|
create_table: 'rows, columns, cellTexts? (2D: rows of cells; each cell is a plain string OR a STYLED object {runs:[{text,marks}], marks?} to format the cell — marks: bold/italic/underline/strikethrough/fontFamily/fontSize(points)/color(hex)/highlight), placement?, changeMode? — style cells HERE, do not format_text afterward; changeMode:"tracked" makes the table insertion itself a tracked change',
|
|
65
|
-
rewrite_block: "selector, text, changeMode? — REPLACES the whole block text
|
|
65
|
+
rewrite_block: "selector, text, changeMode? — REPLACES the whole block text using the document API's formatting preservation. For appending a NEW paragraph/clause use insert_paragraphs, and for small wording edits prefer replace_text with a minimal find span",
|
|
66
66
|
fill_placeholders: 'values[] and/or fields[{label?,value}], changeMode?',
|
|
67
67
|
move_range: 'fromText (text in the FIRST block of the range to move), toText? (text in the LAST block — omit to auto-extend across the whole VISUAL SECTION: from fromText up to the next heading-like/ALL-CAPS/bold title), then exactly ONE destination: afterText OR beforeText (text in the block to land after/before). Direct-only today: changeMode:"tracked" fails with no mutation because block-range deletion cannot be tracked. Moves plain paragraph/heading text only; a range containing a table, list, or image is refused with nothing changed. THE way to MOVE a contiguous block range or a visual "section" identified BY TEXT (works on styled-paragraph sections like PREAMBLE / SCHEDULE A that are NOT Word heading nodes). afterText on a heading-like block lands the range after that block\'s WHOLE section.',
|
|
68
68
|
comment_paragraphs: 'commentText, scope?: all|body, excludeBlockQuotes?',
|
|
@@ -223,6 +223,53 @@ export const ACTION_ARGS = {
|
|
|
223
223
|
insert_page_break: ['selector'],
|
|
224
224
|
add_hyperlink: ['text', 'url', 'tooltip'],
|
|
225
225
|
};
|
|
226
|
+
export const ACTION_DISPOSITIONS = {
|
|
227
|
+
insert_paragraphs: 'tracked',
|
|
228
|
+
insert_heading: 'tracked',
|
|
229
|
+
replace_text: 'tracked',
|
|
230
|
+
delete_text: 'tracked',
|
|
231
|
+
delete_blocks: 'tracked',
|
|
232
|
+
append_list: 'tracked',
|
|
233
|
+
create_table: 'tracked',
|
|
234
|
+
comment_paragraphs: 'direct',
|
|
235
|
+
add_comments: 'direct',
|
|
236
|
+
resolve_comments: 'direct',
|
|
237
|
+
reply_to_comment: 'direct',
|
|
238
|
+
rewrite_block: 'tracked',
|
|
239
|
+
accept_tracked_changes: 'direct',
|
|
240
|
+
reject_tracked_changes: 'direct',
|
|
241
|
+
normalize_body_font_size: 'tracked',
|
|
242
|
+
set_font_family: 'tracked',
|
|
243
|
+
apply_letter_spacing: 'tracked',
|
|
244
|
+
fill_placeholders: 'tracked',
|
|
245
|
+
move_range: 'direct',
|
|
246
|
+
insert_toc: 'tracked',
|
|
247
|
+
insert_table_row: 'tracked',
|
|
248
|
+
insert_table_column: 'tracked',
|
|
249
|
+
delete_table_row: 'tracked',
|
|
250
|
+
delete_table_column: 'tracked',
|
|
251
|
+
split_table: 'tracked',
|
|
252
|
+
convert_list: 'tracked',
|
|
253
|
+
split_list: 'direct',
|
|
254
|
+
undo_changes: 'direct',
|
|
255
|
+
redo_changes: 'direct',
|
|
256
|
+
attach_numbering: 'tracked',
|
|
257
|
+
add_list_items: 'tracked',
|
|
258
|
+
format_text: 'tracked',
|
|
259
|
+
apply_style: 'direct',
|
|
260
|
+
format_paragraph: 'tracked',
|
|
261
|
+
move_text: 'tracked',
|
|
262
|
+
style_table: 'direct',
|
|
263
|
+
move_table: 'direct',
|
|
264
|
+
delete_table: 'tracked',
|
|
265
|
+
set_paragraph_spacing: 'direct',
|
|
266
|
+
insert_page_break: 'direct',
|
|
267
|
+
add_hyperlink: 'direct',
|
|
268
|
+
};
|
|
269
|
+
/** Fresh copy of {@link ACTION_DISPOSITIONS} for integrators (safe to mutate). */
|
|
270
|
+
export function getActionDispositions() {
|
|
271
|
+
return { ...ACTION_DISPOSITIONS };
|
|
272
|
+
}
|
|
226
273
|
export function isActionName(value) {
|
|
227
274
|
return typeof value === 'string' && ACTION_NAMES.includes(value);
|
|
228
275
|
}
|
|
@@ -638,8 +685,8 @@ function evaluateChecks(pre, post, checks) {
|
|
|
638
685
|
else if (check.kind === 'tracked-change-count-delta') {
|
|
639
686
|
results.push({
|
|
640
687
|
check,
|
|
641
|
-
passed: post.trackedChanges
|
|
642
|
-
detail: `pre=${pre.trackedChanges
|
|
688
|
+
passed: post.counts.trackedChanges - pre.counts.trackedChanges === check.delta,
|
|
689
|
+
detail: `pre=${pre.counts.trackedChanges} post=${post.counts.trackedChanges}`,
|
|
643
690
|
});
|
|
644
691
|
}
|
|
645
692
|
else if (check.kind === 'revision-changed') {
|
|
@@ -1146,11 +1193,6 @@ async function runReplaceTextUnchecked(doc, args) {
|
|
|
1146
1193
|
let skippedEdits = [];
|
|
1147
1194
|
let steps;
|
|
1148
1195
|
let applyExpectedRevision;
|
|
1149
|
-
// Blocks whose text this call rewrites, with their pre-mutation run layout:
|
|
1150
|
-
// the engine replace lane renders the replaced range as uniform runs, so a
|
|
1151
|
-
// whole-paragraph find/replace flattens intra-paragraph formatting (bold
|
|
1152
|
-
// lead-ins). Restore it positionally after the mutation.
|
|
1153
|
-
const preserveTargets = [];
|
|
1154
1196
|
if (args.selector) {
|
|
1155
1197
|
const target = selectorToBlockTarget(args.selector, pre);
|
|
1156
1198
|
if (!target) {
|
|
@@ -1189,13 +1231,6 @@ async function runReplaceTextUnchecked(doc, args) {
|
|
|
1189
1231
|
];
|
|
1190
1232
|
applyExpectedRevision = pre.revision;
|
|
1191
1233
|
selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
|
|
1192
|
-
preserveTargets.push({
|
|
1193
|
-
nodeId: target.nodeId,
|
|
1194
|
-
nodeType: target.nodeType ?? 'paragraph',
|
|
1195
|
-
oldText: target.text,
|
|
1196
|
-
newText: rewrittenText,
|
|
1197
|
-
runs: await captureBlockRuns(doc, target.nodeId, target.text),
|
|
1198
|
-
});
|
|
1199
1234
|
}
|
|
1200
1235
|
else {
|
|
1201
1236
|
let candidates = args.edits.map((edit, editIndex) => ({
|
|
@@ -1253,12 +1288,6 @@ async function runReplaceTextUnchecked(doc, args) {
|
|
|
1253
1288
|
steps = candidates.map((candidate) => candidate.step);
|
|
1254
1289
|
}
|
|
1255
1290
|
const result = await executeMutations(doc, steps, args.changeMode, applyExpectedRevision);
|
|
1256
|
-
const preserved = [];
|
|
1257
|
-
for (const entry of preserveTargets) {
|
|
1258
|
-
const restored = await preserveRunPatternAfterRewrite(doc, entry.nodeId, entry.oldText, entry.runs, entry.newText);
|
|
1259
|
-
if (restored)
|
|
1260
|
-
preserved.push({ nodeId: entry.nodeId, ...restored });
|
|
1261
|
-
}
|
|
1262
1291
|
if (args.selector && selectedTargets[0]) {
|
|
1263
1292
|
const post = await buildMutationSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] });
|
|
1264
1293
|
const blockId = selectedTargets[0].matched[0];
|
|
@@ -1287,10 +1316,7 @@ async function runReplaceTextUnchecked(doc, args) {
|
|
|
1287
1316
|
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
1288
1317
|
postSnapshot: { revision: post.revision, counts: post.counts },
|
|
1289
1318
|
selectedTargets,
|
|
1290
|
-
executedOperations: [
|
|
1291
|
-
{ operationId: 'doc.mutations.apply', result },
|
|
1292
|
-
...(preserved.length > 0 ? [{ operationId: 'doc.format.apply', result: { preserved } }] : []),
|
|
1293
|
-
],
|
|
1319
|
+
executedOperations: [{ operationId: 'doc.mutations.apply', result }],
|
|
1294
1320
|
verification,
|
|
1295
1321
|
};
|
|
1296
1322
|
}
|
|
@@ -1314,7 +1340,6 @@ async function runReplaceTextUnchecked(doc, args) {
|
|
|
1314
1340
|
...(skippedEditRationale ? { rationale: skippedEditRationale } : {}),
|
|
1315
1341
|
result,
|
|
1316
1342
|
},
|
|
1317
|
-
...(preserved.length > 0 ? [{ operationId: 'doc.format.apply', result: { preserved } }] : []),
|
|
1318
1343
|
],
|
|
1319
1344
|
verification,
|
|
1320
1345
|
};
|
|
@@ -1563,11 +1588,25 @@ async function runDeleteBlocks(doc, args) {
|
|
|
1563
1588
|
const changeMode = parseChangeMode(args.changeMode);
|
|
1564
1589
|
const executedOperations = [];
|
|
1565
1590
|
const deleted = [];
|
|
1591
|
+
const addressedTrackedChangeIds = new Set();
|
|
1592
|
+
let allTrackedEffectsAddressed = true;
|
|
1566
1593
|
for (const target of targets) {
|
|
1567
1594
|
try {
|
|
1568
1595
|
const result = await deleteFn({ target: { kind: 'block', nodeType: target.nodeType, nodeId: target.nodeId } }, changeMode ? { changeMode } : undefined);
|
|
1569
1596
|
executedOperations.push({ operationId: 'doc.blocks.delete', result: compactOpResult(result) });
|
|
1570
1597
|
deleted.push({ nodeId: target.nodeId, nodeType: target.nodeType });
|
|
1598
|
+
if (changeMode === 'tracked') {
|
|
1599
|
+
// One block transaction can create multiple logical review entities
|
|
1600
|
+
// (for example, text plus its section break). Exact receipt refs let
|
|
1601
|
+
// us fail closed on missing entities. Older handles omit refs, and
|
|
1602
|
+
// adjacent deletions can coalesce into one review entity, so their
|
|
1603
|
+
// truthful fallback is the positive observed inventory delta rather
|
|
1604
|
+
// than one assumed entity per operation.
|
|
1605
|
+
const refIds = trackedChangeRefIds(result);
|
|
1606
|
+
allTrackedEffectsAddressed &&= refIds.size > 0;
|
|
1607
|
+
for (const entityId of refIds)
|
|
1608
|
+
addressedTrackedChangeIds.add(entityId);
|
|
1609
|
+
}
|
|
1571
1610
|
}
|
|
1572
1611
|
catch (err) {
|
|
1573
1612
|
errors.push({
|
|
@@ -1581,14 +1620,37 @@ async function runDeleteBlocks(doc, args) {
|
|
|
1581
1620
|
// Tracked deletions leave the block in place until the revision is decided,
|
|
1582
1621
|
// so the block count cannot move — count the structural revisions instead.
|
|
1583
1622
|
// Direct deletions must show one fewer block of each deleted node type.
|
|
1623
|
+
const observedTrackedChangeDelta = post.counts.trackedChanges - pre.counts.trackedChanges;
|
|
1624
|
+
// A later deletion can coalesce the batch and replace entity identities
|
|
1625
|
+
// returned by earlier operation receipts. For a multi-operation batch the
|
|
1626
|
+
// final inventory is therefore the authoritative count. A single operation
|
|
1627
|
+
// keeps using its addressed refs so fan-out remains verifiable even when
|
|
1628
|
+
// the inventory listing is capped.
|
|
1629
|
+
const expectedTrackedChangeDelta = deleted.length > 1
|
|
1630
|
+
? observedTrackedChangeDelta
|
|
1631
|
+
: allTrackedEffectsAddressed
|
|
1632
|
+
? addressedTrackedChangeIds.size
|
|
1633
|
+
: Math.max(observedTrackedChangeDelta, addressedTrackedChangeIds.size);
|
|
1584
1634
|
const checks = changeMode === 'tracked'
|
|
1585
|
-
? [
|
|
1635
|
+
? [
|
|
1636
|
+
{
|
|
1637
|
+
kind: 'tracked-change-count-delta',
|
|
1638
|
+
delta: expectedTrackedChangeDelta,
|
|
1639
|
+
},
|
|
1640
|
+
]
|
|
1586
1641
|
: [...new Set(deleted.map((entry) => entry.nodeType))].map((nodeType) => ({
|
|
1587
1642
|
kind: 'block-count-delta',
|
|
1588
1643
|
nodeType,
|
|
1589
1644
|
delta: -deleted.filter((entry) => entry.nodeType === nodeType).length,
|
|
1590
1645
|
}));
|
|
1591
1646
|
const verification = evaluateChecks(pre, post, checks);
|
|
1647
|
+
if (changeMode === 'tracked' && observedTrackedChangeDelta <= 0) {
|
|
1648
|
+
verification[0] = {
|
|
1649
|
+
...verification[0],
|
|
1650
|
+
passed: false,
|
|
1651
|
+
detail: `pre=${pre.counts.trackedChanges} post=${post.counts.trackedChanges}; no tracked review entity was created`,
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1592
1654
|
// Compare against the DEDUPED target list: two selectors naming the same
|
|
1593
1655
|
// block are one deletion, and counting the raw selectors would report a
|
|
1594
1656
|
// fully applied call as `partial` with no error to explain it. Selectors
|
|
@@ -2587,7 +2649,6 @@ async function runRewriteBlock(doc, args) {
|
|
|
2587
2649
|
throw new Error('selector did not resolve to a body block');
|
|
2588
2650
|
context.selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
|
|
2589
2651
|
const text = normalizeTitleLikeRewriteText(target.text, args.text);
|
|
2590
|
-
const runs = await captureBlockRuns(doc, target.nodeId, target.text);
|
|
2591
2652
|
await executeMutations(context.document, [
|
|
2592
2653
|
{
|
|
2593
2654
|
id: 'rewrite-block-1',
|
|
@@ -2596,7 +2657,6 @@ async function runRewriteBlock(doc, args) {
|
|
|
2596
2657
|
args: { replacement: { text }, style: preserveRewriteStyle() },
|
|
2597
2658
|
},
|
|
2598
2659
|
], args.changeMode, pre.revision);
|
|
2599
|
-
await preserveRunPatternAfterRewrite(context.document, target.nodeId, target.text, runs, text);
|
|
2600
2660
|
const post = await context.finish();
|
|
2601
2661
|
const rewritten = await post.block(target.nodeId);
|
|
2602
2662
|
const verification = [
|
|
@@ -2669,23 +2729,8 @@ async function captureBlockRuns(doc, nodeId, text) {
|
|
|
2669
2729
|
}
|
|
2670
2730
|
}
|
|
2671
2731
|
/**
|
|
2672
|
-
*
|
|
2673
|
-
*
|
|
2674
|
-
* drops intra-paragraph formatting AND can smear the first run's marks over
|
|
2675
|
-
* everything. Marks are restored positionally over the parts of the text the
|
|
2676
|
-
* rewrite did NOT change (the common prefix and suffix of old vs new text),
|
|
2677
|
-
* and the changed middle takes the marks of the run it continues from —
|
|
2678
|
-
* Word's "typing continues formatting" rule. Best-effort: never fails the
|
|
2679
|
-
* rewrite.
|
|
2680
|
-
*/
|
|
2681
|
-
/**
|
|
2682
|
-
* Span-aware run-pattern restore for scoped replaces. The generic
|
|
2683
|
-
* preserveRunPatternAfterRewrite diffs prefix/suffix and treats everything
|
|
2684
|
-
* between the FIRST and LAST change as one modified middle — with two
|
|
2685
|
-
* separated replacements that flattens independently-styled runs between
|
|
2686
|
-
* them. Here the exact spans are known, so every untouched interval keeps its
|
|
2687
|
-
* original run formatting at the offset-mapped position, and each replacement
|
|
2688
|
-
* inherits the marks of the run it starts in.
|
|
2732
|
+
* Restore scoped replacements from their known spans: each untouched interval
|
|
2733
|
+
* keeps its run formatting, and replacements inherit the marks at their start.
|
|
2689
2734
|
* `spans` are FULL-TEXT coordinates, ascending, non-overlapping.
|
|
2690
2735
|
*/
|
|
2691
2736
|
async function preserveScopedRunPattern(doc, nodeId, oldText, oldRuns, spans) {
|
|
@@ -2778,74 +2823,6 @@ async function preserveScopedRunPattern(doc, nodeId, oldText, oldRuns, spans) {
|
|
|
2778
2823
|
return { skipped: reasonOf(err) };
|
|
2779
2824
|
}
|
|
2780
2825
|
}
|
|
2781
|
-
async function preserveRunPatternAfterRewrite(doc, nodeId, oldText, oldRuns, newText) {
|
|
2782
|
-
// Nothing to restore when the block was uniformly plain; a single STYLED run
|
|
2783
|
-
// still needs restoring (whole-block replaces drop even uniform marks).
|
|
2784
|
-
if (!oldRuns || oldRuns.length === 0)
|
|
2785
|
-
return null;
|
|
2786
|
-
if (!oldRuns.some((run) => Object.keys(inlineFromSnapshotRun(run, new Set())).length > 0))
|
|
2787
|
-
return null;
|
|
2788
|
-
const formatFn = maybeMethod(doc, ['format', 'apply']);
|
|
2789
|
-
if (!formatFn)
|
|
2790
|
-
return null;
|
|
2791
|
-
// Boolean marks any old run carried: plain ranges must explicitly clear
|
|
2792
|
-
// these, because the rewritten single run may have smeared them everywhere.
|
|
2793
|
-
const normalize = new Set(RUN_BOOL_MARKS.filter((key) => oldRuns.some((run) => run[key])));
|
|
2794
|
-
// query.match run offsets are relative to the trimmed text used to match.
|
|
2795
|
-
const oldTrim = oldText.trim();
|
|
2796
|
-
const newTrimStart = newText.length - newText.trimStart().length;
|
|
2797
|
-
const newTrim = newText.trim();
|
|
2798
|
-
let prefix = 0;
|
|
2799
|
-
const maxCommon = Math.min(oldTrim.length, newTrim.length);
|
|
2800
|
-
while (prefix < maxCommon && oldTrim[prefix] === newTrim[prefix])
|
|
2801
|
-
prefix += 1;
|
|
2802
|
-
let suffix = 0;
|
|
2803
|
-
while (suffix < maxCommon - prefix && oldTrim[oldTrim.length - 1 - suffix] === newTrim[newTrim.length - 1 - suffix])
|
|
2804
|
-
suffix += 1;
|
|
2805
|
-
const delta = newTrim.length - oldTrim.length;
|
|
2806
|
-
let applied = 0;
|
|
2807
|
-
const applyRange = async (start, end, inline) => {
|
|
2808
|
-
if (end <= start || Object.keys(inline).length === 0)
|
|
2809
|
-
return;
|
|
2810
|
-
// Always direct, even under tracked mode. This pass RESTORES the marks the
|
|
2811
|
-
// rewrite flattened; it is not new authorial formatting, and the text
|
|
2812
|
-
// revision already owns the span's accept/reject fate. Applying it tracked
|
|
2813
|
-
// adds a second reviewable change per rewrite (SD-4309's conformance matrix
|
|
2814
|
-
// counts one per action), and rejecting that change alone would strand the
|
|
2815
|
-
// text in the flattened look the rewrite produced.
|
|
2816
|
-
const changed = await applyInlineQuiet(formatFn, {
|
|
2817
|
-
blockId: nodeId,
|
|
2818
|
-
start: newTrimStart + start,
|
|
2819
|
-
end: newTrimStart + end,
|
|
2820
|
-
inline,
|
|
2821
|
-
});
|
|
2822
|
-
if (changed)
|
|
2823
|
-
applied += 1;
|
|
2824
|
-
};
|
|
2825
|
-
try {
|
|
2826
|
-
for (const run of oldRuns) {
|
|
2827
|
-
const inline = inlineFromSnapshotRun(run, normalize);
|
|
2828
|
-
// Portion of the run inside the unchanged prefix.
|
|
2829
|
-
await applyRange(run.start, Math.min(run.end, prefix), inline);
|
|
2830
|
-
// Portion inside the unchanged suffix, shifted by the length delta.
|
|
2831
|
-
const sufStart = Math.max(run.start, oldTrim.length - suffix);
|
|
2832
|
-
if (run.end > sufStart)
|
|
2833
|
-
await applyRange(sufStart + delta, run.end + delta, inline);
|
|
2834
|
-
}
|
|
2835
|
-
// The CHANGED middle keeps whatever the engine smeared; give it the marks
|
|
2836
|
-
// of the run the change continues from (the one containing the last
|
|
2837
|
-
// unchanged prefix character).
|
|
2838
|
-
const middleEnd = newTrim.length - suffix;
|
|
2839
|
-
if (middleEnd > prefix) {
|
|
2840
|
-
const anchorRun = oldRuns.find((run) => prefix > run.start && prefix <= run.end) ?? oldRuns[oldRuns.length - 1];
|
|
2841
|
-
await applyRange(prefix, middleEnd, inlineFromSnapshotRun(anchorRun, normalize));
|
|
2842
|
-
}
|
|
2843
|
-
return applied > 0 ? { restoredRunRanges: applied } : null;
|
|
2844
|
-
}
|
|
2845
|
-
catch (err) {
|
|
2846
|
-
return { skipped: reasonOf(err) };
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
2826
|
const NAMED_COLORS = {
|
|
2850
2827
|
black: '000000',
|
|
2851
2828
|
blue: '0000FF',
|
|
@@ -4617,6 +4594,15 @@ function compactOpResult(result) {
|
|
|
4617
4594
|
compact.stepCount = result.steps.length;
|
|
4618
4595
|
return compact;
|
|
4619
4596
|
}
|
|
4597
|
+
function trackedChangeRefIds(result) {
|
|
4598
|
+
const refs = asRecord(result)?.trackedChangeRefs;
|
|
4599
|
+
if (!Array.isArray(refs))
|
|
4600
|
+
return new Set();
|
|
4601
|
+
return new Set(refs.flatMap((entry) => {
|
|
4602
|
+
const entityId = asString(asRecord(entry)?.entityId);
|
|
4603
|
+
return entityId ? [entityId] : [];
|
|
4604
|
+
}));
|
|
4605
|
+
}
|
|
4620
4606
|
function parseScopedReplaceArgs(args, changeMode) {
|
|
4621
4607
|
if (args.action !== 'replace_text')
|
|
4622
4608
|
return null;
|
|
@@ -4730,9 +4716,8 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4730
4716
|
},
|
|
4731
4717
|
args: { replacement: { text: span.replace } },
|
|
4732
4718
|
}));
|
|
4733
|
-
//
|
|
4734
|
-
//
|
|
4735
|
-
// applies directly, so restoring marks adds no second reviewable change.
|
|
4719
|
+
// Restore the known edit spans directly so the formatting pass adds no
|
|
4720
|
+
// separate tracked change.
|
|
4736
4721
|
const trackedPreRuns = await captureBlockRuns(doc, nodeId, block.text);
|
|
4737
4722
|
try {
|
|
4738
4723
|
result = await executeMutations(doc, spanSteps, args.changeMode, pre.revision);
|
|
@@ -4922,10 +4907,24 @@ function leadInShapeOf(blockText, labelText, quote) {
|
|
|
4922
4907
|
if (occurrence > 0)
|
|
4923
4908
|
return { kind: 'inside', delimiter: inside, occurrence };
|
|
4924
4909
|
}
|
|
4910
|
+
// A single emphasised word followed by a space and plain prose: the token
|
|
4911
|
+
// itself is the label ("WHEREAS the Party…"). Multi-word runs stay
|
|
4912
|
+
// unrecognised — an emphasised opening clause is prose, not a label.
|
|
4913
|
+
if (/^\S+\s$/.test(labelText) && blockText.length > labelText.length && blockText.startsWith(labelText)) {
|
|
4914
|
+
return { kind: 'word', label: labelText.trimEnd() };
|
|
4915
|
+
}
|
|
4925
4916
|
return null;
|
|
4926
4917
|
}
|
|
4927
4918
|
/** The span of `text` that carries the label look, or null when it has none. */
|
|
4928
4919
|
function leadInSpanOf(text, shape) {
|
|
4920
|
+
if (shape.kind === 'word') {
|
|
4921
|
+
// Only the SAME token inherits the look — "WHEREAS the Parties…" does,
|
|
4922
|
+
// "The Parties further agree…" does not (no guessing a label there).
|
|
4923
|
+
const next = text.charAt(shape.label.length);
|
|
4924
|
+
return text.startsWith(shape.label) && (next === '' || /\s/.test(next))
|
|
4925
|
+
? { start: 0, end: shape.label.length }
|
|
4926
|
+
: null;
|
|
4927
|
+
}
|
|
4929
4928
|
if (shape.kind === 'quoted') {
|
|
4930
4929
|
const opener = text.slice(0, 2).indexOf(shape.quote);
|
|
4931
4930
|
if (opener < 0)
|
package/dist/agent/catalog.cjs
CHANGED
|
@@ -543,18 +543,61 @@ function toProviderTool(provider, definition) {
|
|
|
543
543
|
return toVercelTool(definition);
|
|
544
544
|
return toOpenAiTool(definition);
|
|
545
545
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
for (const name of
|
|
546
|
+
/** Wildcard entry for `allowDirectActions`: keep every direct-only action callable. */
|
|
547
|
+
const ALLOW_ALL_DIRECT_ACTIONS = '*';
|
|
548
|
+
function assertKnownActions(option, names, knownActions) {
|
|
549
|
+
for (const name of names) {
|
|
550
550
|
if (!knownActions.has(name)) {
|
|
551
|
-
throw new errors.SuperDocCliError(
|
|
551
|
+
throw new errors.SuperDocCliError(`${option}: unknown action "${name}".`, {
|
|
552
552
|
code: 'INVALID_ARGUMENT',
|
|
553
553
|
details: { unknownAction: name, knownActions: [...knownActions] },
|
|
554
554
|
});
|
|
555
555
|
}
|
|
556
556
|
}
|
|
557
|
-
|
|
557
|
+
}
|
|
558
|
+
function resolveActionPolicy(options, resolveOptions) {
|
|
559
|
+
const knownActions = new Set(actions.ACTION_NAMES_LIST);
|
|
560
|
+
const ignoreUnknown = resolveOptions?.ignoreUnknownActions === true;
|
|
561
|
+
const configuredExclusions = new Set(options?.excludeActions ?? []);
|
|
562
|
+
if (!ignoreUnknown)
|
|
563
|
+
assertKnownActions('excludeActions', configuredExclusions, knownActions);
|
|
564
|
+
const enforceChangeMode = options?.enforceChangeMode;
|
|
565
|
+
if (enforceChangeMode !== undefined && enforceChangeMode !== 'tracked') {
|
|
566
|
+
throw new errors.SuperDocCliError(`enforceChangeMode: only "tracked" is supported; got ${JSON.stringify(enforceChangeMode)}.`, {
|
|
567
|
+
code: 'INVALID_ARGUMENT',
|
|
568
|
+
details: { enforceChangeMode, supported: ['tracked'] },
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
if (options?.allowDirectActions !== undefined && enforceChangeMode === undefined) {
|
|
572
|
+
throw new errors.SuperDocCliError('allowDirectActions requires enforceChangeMode: "tracked"; without enforcement every action already runs as requested.', {
|
|
573
|
+
code: 'INVALID_ARGUMENT',
|
|
574
|
+
details: { allowDirectActions: [...options.allowDirectActions] },
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
// `'*'` is the "I accept every direct-only action" shortcut: tracked-capable
|
|
578
|
+
// actions are still forced tracked, the direct bucket just stays callable.
|
|
579
|
+
const requestedAllow = new Set(options?.allowDirectActions ?? []);
|
|
580
|
+
const allowAllDirect = requestedAllow.delete(ALLOW_ALL_DIRECT_ACTIONS);
|
|
581
|
+
if (!ignoreUnknown)
|
|
582
|
+
assertKnownActions('allowDirectActions', requestedAllow, knownActions);
|
|
583
|
+
const allowDirectActions = new Set((allowAllDirect ? [...actions.ACTION_NAMES_LIST] : [...requestedAllow]).filter((name) => actions.ACTION_DISPOSITIONS[name] === 'direct'));
|
|
584
|
+
const directOnlyExclusions = new Set();
|
|
585
|
+
if (enforceChangeMode === 'tracked') {
|
|
586
|
+
for (const name of actions.ACTION_NAMES_LIST) {
|
|
587
|
+
if (actions.ACTION_DISPOSITIONS[name] !== 'direct')
|
|
588
|
+
continue;
|
|
589
|
+
if (configuredExclusions.has(name) || allowDirectActions.has(name))
|
|
590
|
+
continue;
|
|
591
|
+
directOnlyExclusions.add(name);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
excludedActions: new Set([...configuredExclusions, ...directOnlyExclusions]),
|
|
596
|
+
configuredExclusions,
|
|
597
|
+
directOnlyExclusions,
|
|
598
|
+
enforceChangeMode,
|
|
599
|
+
allowDirectActions,
|
|
600
|
+
};
|
|
558
601
|
}
|
|
559
602
|
/**
|
|
560
603
|
* Tools advertised to the LLM. Only PUBLIC_AGENT_TOOL_NAMES (currently
|
|
@@ -566,7 +609,7 @@ function validateExclusions(options) {
|
|
|
566
609
|
* don't want the model to see certain capabilities.
|
|
567
610
|
*/
|
|
568
611
|
function listAgentTools(provider, options) {
|
|
569
|
-
const { excludedActions } =
|
|
612
|
+
const { excludedActions } = resolveActionPolicy(options);
|
|
570
613
|
const publicSet = new Set(PUBLIC_AGENT_TOOL_NAMES);
|
|
571
614
|
const includedActions = actions.ACTION_NAMES_LIST.filter((name) => !excludedActions.has(name));
|
|
572
615
|
return AGENT_TOOL_DEFINITIONS.filter((d) => publicSet.has(d.name))
|
|
@@ -587,6 +630,7 @@ function isAgentToolName(toolName) {
|
|
|
587
630
|
exports.ACTION_ARG_SCHEMA = ACTION_ARG_SCHEMA;
|
|
588
631
|
exports.AGENT_TOOL_DEFINITIONS = AGENT_TOOL_DEFINITIONS;
|
|
589
632
|
exports.AGENT_TOOL_NAMES = AGENT_TOOL_NAMES;
|
|
633
|
+
exports.ALLOW_ALL_DIRECT_ACTIONS = ALLOW_ALL_DIRECT_ACTIONS;
|
|
590
634
|
exports.CELL_SCHEMA = CELL_SCHEMA;
|
|
591
635
|
exports.LIST_ITEM_SCHEMA = LIST_ITEM_SCHEMA;
|
|
592
636
|
exports.PUBLIC_AGENT_TOOL_NAMES = PUBLIC_AGENT_TOOL_NAMES;
|
|
@@ -595,4 +639,4 @@ exports.SELECTOR_SCHEMA = SELECTOR_SCHEMA;
|
|
|
595
639
|
exports.buildPerformActionDefinition = buildPerformActionDefinition;
|
|
596
640
|
exports.isAgentToolName = isAgentToolName;
|
|
597
641
|
exports.listAgentTools = listAgentTools;
|
|
598
|
-
exports.
|
|
642
|
+
exports.resolveActionPolicy = resolveActionPolicy;
|
package/dist/agent/catalog.d.ts
CHANGED
|
@@ -326,7 +326,15 @@ export declare const ACTION_ARG_SCHEMA: Record<string, unknown>;
|
|
|
326
326
|
*/
|
|
327
327
|
export declare function buildPerformActionDefinition(includedActions: readonly string[]): AgentToolDefinition;
|
|
328
328
|
export declare const AGENT_TOOL_DEFINITIONS: readonly AgentToolDefinition[];
|
|
329
|
-
/**
|
|
329
|
+
/** The only change mode a host can enforce today. */
|
|
330
|
+
export type EnforceChangeMode = 'tracked';
|
|
331
|
+
/** Wildcard entry for `allowDirectActions`: keep every direct-only action callable. */
|
|
332
|
+
export declare const ALLOW_ALL_DIRECT_ACTIONS = "*";
|
|
333
|
+
/**
|
|
334
|
+
* Customer-facing narrowing of the advertised tool surface. The same options
|
|
335
|
+
* shape the tool schema, the system prompt, and the dispatch guard so the
|
|
336
|
+
* three can never disagree.
|
|
337
|
+
*/
|
|
330
338
|
export interface ListAgentToolsOptions {
|
|
331
339
|
/**
|
|
332
340
|
* Action names to REMOVE from `superdoc_perform_action` — the enum, the
|
|
@@ -335,7 +343,52 @@ export interface ListAgentToolsOptions {
|
|
|
335
343
|
* every action drops the tool entirely.
|
|
336
344
|
*/
|
|
337
345
|
excludeActions?: readonly string[];
|
|
346
|
+
/**
|
|
347
|
+
* Fail-closed tracked-changes-only policy. With `'tracked'`, every action
|
|
348
|
+
* whose disposition is `direct` (see ACTION_DISPOSITIONS) is removed from the
|
|
349
|
+
* surface exactly like an excluded action, and every tracked-capable action
|
|
350
|
+
* is upgraded to `changeMode: "tracked"` at dispatch whatever the model
|
|
351
|
+
* asked for. Only `'tracked'` is accepted.
|
|
352
|
+
*/
|
|
353
|
+
enforceChangeMode?: EnforceChangeMode;
|
|
354
|
+
/**
|
|
355
|
+
* Direct-only actions a host permits under `enforceChangeMode`. They stay in
|
|
356
|
+
* the enum and prompt and run directly at dispatch. `['*']` allows every
|
|
357
|
+
* direct-only action (tracked-capable ones are still forced tracked).
|
|
358
|
+
* Requires `enforceChangeMode`; unknown names throw; tracked-capable names
|
|
359
|
+
* are ignored so a list survives an action gaining tracked support.
|
|
360
|
+
*/
|
|
361
|
+
allowDirectActions?: readonly string[];
|
|
362
|
+
}
|
|
363
|
+
/** The exclusion set a policy resolves to, split by why each action is out. */
|
|
364
|
+
export interface ResolvedActionPolicy {
|
|
365
|
+
/** Every action removed from the enum, prompt, and dispatch. */
|
|
366
|
+
excludedActions: Set<string>;
|
|
367
|
+
/** Removed by `excludeActions`. Wins over `allowDirectActions`. */
|
|
368
|
+
configuredExclusions: Set<string>;
|
|
369
|
+
/** Removed because it is direct-only under `enforceChangeMode` and not allowlisted. */
|
|
370
|
+
directOnlyExclusions: Set<string>;
|
|
371
|
+
enforceChangeMode: EnforceChangeMode | undefined;
|
|
372
|
+
/** Direct-only actions the host allowlisted (validated, tracked names dropped). */
|
|
373
|
+
allowDirectActions: Set<string>;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Validate and resolve the surface-narrowing options into one exclusion set.
|
|
377
|
+
* Every layer (schema, prompt, dispatch) calls this so the policy is computed
|
|
378
|
+
* exactly one way.
|
|
379
|
+
*/
|
|
380
|
+
export interface ResolveActionPolicyOptions {
|
|
381
|
+
/**
|
|
382
|
+
* Skip the unknown-name check. A preset built on top of core (custom actions)
|
|
383
|
+
* forwards ONE exclusion list that mixes its own action names with core's, so
|
|
384
|
+
* at dispatch time a name core does not own is another layer's business, not
|
|
385
|
+
* a caller mistake. Configure-time entry points (getTools, getSystemPrompt)
|
|
386
|
+
* leave this off so a typo still fails loudly where it was written.
|
|
387
|
+
*/
|
|
388
|
+
ignoreUnknownActions?: boolean;
|
|
338
389
|
}
|
|
390
|
+
export declare function resolveActionPolicy(options?: ListAgentToolsOptions, resolveOptions?: ResolveActionPolicyOptions): ResolvedActionPolicy;
|
|
391
|
+
/** @deprecated Use {@link resolveActionPolicy}; kept for callers that only need the union. */
|
|
339
392
|
export declare function validateExclusions(options?: ListAgentToolsOptions): {
|
|
340
393
|
excludedActions: Set<string>;
|
|
341
394
|
};
|
package/dist/agent/catalog.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SuperDocCliError } from '../runtime/errors.js';
|
|
2
|
-
import { ACTION_NAMES_LIST, ACTION_HINTS, ACTION_GROUPS, ACTION_ARGS } from './actions.js';
|
|
2
|
+
import { ACTION_NAMES_LIST, ACTION_HINTS, ACTION_GROUPS, ACTION_ARGS, ACTION_DISPOSITIONS } from './actions.js';
|
|
3
3
|
export const AGENT_TOOL_NAMES = [
|
|
4
4
|
'superdoc_inspect',
|
|
5
5
|
'superdoc_perform_action',
|
|
@@ -540,18 +540,65 @@ function toProviderTool(provider, definition) {
|
|
|
540
540
|
return toVercelTool(definition);
|
|
541
541
|
return toOpenAiTool(definition);
|
|
542
542
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
for (const name of
|
|
543
|
+
/** Wildcard entry for `allowDirectActions`: keep every direct-only action callable. */
|
|
544
|
+
export const ALLOW_ALL_DIRECT_ACTIONS = '*';
|
|
545
|
+
function assertKnownActions(option, names, knownActions) {
|
|
546
|
+
for (const name of names) {
|
|
547
547
|
if (!knownActions.has(name)) {
|
|
548
|
-
throw new SuperDocCliError(
|
|
548
|
+
throw new SuperDocCliError(`${option}: unknown action "${name}".`, {
|
|
549
549
|
code: 'INVALID_ARGUMENT',
|
|
550
550
|
details: { unknownAction: name, knownActions: [...knownActions] },
|
|
551
551
|
});
|
|
552
552
|
}
|
|
553
553
|
}
|
|
554
|
-
|
|
554
|
+
}
|
|
555
|
+
export function resolveActionPolicy(options, resolveOptions) {
|
|
556
|
+
const knownActions = new Set(ACTION_NAMES_LIST);
|
|
557
|
+
const ignoreUnknown = resolveOptions?.ignoreUnknownActions === true;
|
|
558
|
+
const configuredExclusions = new Set(options?.excludeActions ?? []);
|
|
559
|
+
if (!ignoreUnknown)
|
|
560
|
+
assertKnownActions('excludeActions', configuredExclusions, knownActions);
|
|
561
|
+
const enforceChangeMode = options?.enforceChangeMode;
|
|
562
|
+
if (enforceChangeMode !== undefined && enforceChangeMode !== 'tracked') {
|
|
563
|
+
throw new SuperDocCliError(`enforceChangeMode: only "tracked" is supported; got ${JSON.stringify(enforceChangeMode)}.`, {
|
|
564
|
+
code: 'INVALID_ARGUMENT',
|
|
565
|
+
details: { enforceChangeMode, supported: ['tracked'] },
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
if (options?.allowDirectActions !== undefined && enforceChangeMode === undefined) {
|
|
569
|
+
throw new SuperDocCliError('allowDirectActions requires enforceChangeMode: "tracked"; without enforcement every action already runs as requested.', {
|
|
570
|
+
code: 'INVALID_ARGUMENT',
|
|
571
|
+
details: { allowDirectActions: [...options.allowDirectActions] },
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
// `'*'` is the "I accept every direct-only action" shortcut: tracked-capable
|
|
575
|
+
// actions are still forced tracked, the direct bucket just stays callable.
|
|
576
|
+
const requestedAllow = new Set(options?.allowDirectActions ?? []);
|
|
577
|
+
const allowAllDirect = requestedAllow.delete(ALLOW_ALL_DIRECT_ACTIONS);
|
|
578
|
+
if (!ignoreUnknown)
|
|
579
|
+
assertKnownActions('allowDirectActions', requestedAllow, knownActions);
|
|
580
|
+
const allowDirectActions = new Set((allowAllDirect ? [...ACTION_NAMES_LIST] : [...requestedAllow]).filter((name) => ACTION_DISPOSITIONS[name] === 'direct'));
|
|
581
|
+
const directOnlyExclusions = new Set();
|
|
582
|
+
if (enforceChangeMode === 'tracked') {
|
|
583
|
+
for (const name of ACTION_NAMES_LIST) {
|
|
584
|
+
if (ACTION_DISPOSITIONS[name] !== 'direct')
|
|
585
|
+
continue;
|
|
586
|
+
if (configuredExclusions.has(name) || allowDirectActions.has(name))
|
|
587
|
+
continue;
|
|
588
|
+
directOnlyExclusions.add(name);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
excludedActions: new Set([...configuredExclusions, ...directOnlyExclusions]),
|
|
593
|
+
configuredExclusions,
|
|
594
|
+
directOnlyExclusions,
|
|
595
|
+
enforceChangeMode,
|
|
596
|
+
allowDirectActions,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/** @deprecated Use {@link resolveActionPolicy}; kept for callers that only need the union. */
|
|
600
|
+
export function validateExclusions(options) {
|
|
601
|
+
return { excludedActions: resolveActionPolicy(options).excludedActions };
|
|
555
602
|
}
|
|
556
603
|
/**
|
|
557
604
|
* Tools advertised to the LLM. Only PUBLIC_AGENT_TOOL_NAMES (currently
|
|
@@ -563,7 +610,7 @@ export function validateExclusions(options) {
|
|
|
563
610
|
* don't want the model to see certain capabilities.
|
|
564
611
|
*/
|
|
565
612
|
export function listAgentTools(provider, options) {
|
|
566
|
-
const { excludedActions } =
|
|
613
|
+
const { excludedActions } = resolveActionPolicy(options);
|
|
567
614
|
const publicSet = new Set(PUBLIC_AGENT_TOOL_NAMES);
|
|
568
615
|
const includedActions = ACTION_NAMES_LIST.filter((name) => !excludedActions.has(name));
|
|
569
616
|
return AGENT_TOOL_DEFINITIONS.filter((d) => publicSet.has(d.name))
|