@superdoc/sdk 2.5.0 → 2.7.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 +1869 -160
- package/dist/agent/actions.d.ts +77 -10
- package/dist/agent/actions.js +1870 -161
- package/dist/agent/catalog.cjs +102 -9
- package/dist/agent/catalog.d.ts +243 -0
- package/dist/agent/catalog.js +99 -9
- package/dist/agent/doc-snapshot.cjs +200 -2
- package/dist/agent/doc-snapshot.d.ts +91 -0
- package/dist/agent/doc-snapshot.js +199 -2
- package/dist/agent/runtime.cjs +9 -1
- package/dist/agent/runtime.d.ts +8 -0
- package/dist/agent/runtime.js +9 -1
- package/dist/embedded-tools.generated.cjs +5 -5
- package/dist/embedded-tools.generated.js +5 -5
- package/dist/generated/client.cjs +754 -770
- package/dist/generated/client.d.ts +14 -9
- package/dist/generated/client.js +754 -770
- package/dist/generated/contract.cjs +16207 -274
- package/dist/generated/contract.d.ts +38 -0
- package/dist/generated/contract.js +17443 -1503
- package/dist/index.cjs +5 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -4
- package/dist/introspection.cjs +59 -0
- package/dist/introspection.d.ts +3 -0
- package/dist/introspection.js +53 -0
- package/dist/runtime/document-rpc.cjs +328 -0
- package/dist/runtime/document-rpc.d.ts +24 -0
- package/dist/runtime/document-rpc.js +312 -0
- package/dist/runtime/embedded-cli.cjs +5 -68
- package/dist/runtime/embedded-cli.js +5 -67
- package/dist/runtime/embedded-document-host.cjs +28 -0
- package/dist/runtime/embedded-document-host.d.ts +1 -0
- package/dist/runtime/embedded-document-host.js +23 -0
- package/dist/runtime/embedded-platform.cjs +102 -0
- package/dist/runtime/embedded-platform.d.ts +5 -0
- package/dist/runtime/embedded-platform.js +93 -0
- package/dist/runtime/host.cjs +128 -16
- package/dist/runtime/host.d.ts +11 -5
- package/dist/runtime/host.js +126 -16
- package/dist/runtime/process.cjs +34 -5
- package/dist/runtime/process.d.ts +12 -3
- package/dist/runtime/process.js +33 -5
- package/dist/runtime/transport-common.cjs +1 -0
- package/dist/runtime/transport-common.d.ts +28 -10
- package/dist/runtime/transport-common.js +1 -1
- package/package.json +7 -6
- package/tools/__pycache__/__init__.cpython-311.pyc +0 -0
- package/tools/__pycache__/intent_dispatch_generated.cpython-311.pyc +0 -0
- package/tools/catalog.json +2 -2
- package/tools/tools-policy.json +1 -1
- package/tools/tools.anthropic.json +2 -2
- package/tools/tools.generic.json +2 -2
- package/tools/tools.openai.json +2 -2
- package/tools/tools.vercel.json +2 -2
|
@@ -18,6 +18,62 @@ async function safeCall(thunk, fallback, onError) {
|
|
|
18
18
|
return fallback;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Read a specific block's per-run formatting via query.match, matching STRICTLY
|
|
23
|
+
* by nodeId. When the text is ambiguous (multiple matches) and none matches the
|
|
24
|
+
* requested nodeId, returns [] rather than guessing a different block — so a
|
|
25
|
+
* duplicate-text sibling can never contribute the wrong runs. The single-match
|
|
26
|
+
* fallback is used only when there is exactly one candidate (unambiguous).
|
|
27
|
+
*/
|
|
28
|
+
async function matchRunsForBlock(queryFn, text, nodeId) {
|
|
29
|
+
const res = asRecord(await queryFn({ select: { type: 'text', pattern: text } }));
|
|
30
|
+
const items = (Array.isArray(res?.items) ? res.items : []).map(asRecord);
|
|
31
|
+
const match = items.find((m) => asString(asRecord(m?.address)?.nodeId) === nodeId) ?? (items.length === 1 ? items[0] : null);
|
|
32
|
+
if (!match)
|
|
33
|
+
return [];
|
|
34
|
+
const blocksArr = Array.isArray(match.blocks) ? match.blocks.map(asRecord) : [];
|
|
35
|
+
const block = blocksArr.find((b) => asString(b?.blockId) === nodeId) ?? (blocksArr.length === 1 ? blocksArr[0] : null);
|
|
36
|
+
if (!block)
|
|
37
|
+
return [];
|
|
38
|
+
const rawRuns = Array.isArray(block.runs) ? block.runs : [];
|
|
39
|
+
return rawRuns.map(projectRun).filter((r) => r != null);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Project one raw query.match run into a compact SnapshotRun. Uses the
|
|
43
|
+
* EFFECTIVE (resolved) mark state and emits booleans only when true. Skips a
|
|
44
|
+
* default-black color so only meaningful colors surface.
|
|
45
|
+
*/
|
|
46
|
+
function projectRun(raw) {
|
|
47
|
+
const rec = asRecord(raw);
|
|
48
|
+
if (!rec)
|
|
49
|
+
return null;
|
|
50
|
+
const range = asRecord(rec.range);
|
|
51
|
+
const styles = asRecord(rec.styles);
|
|
52
|
+
const eff = asRecord(styles?.effective);
|
|
53
|
+
const run = {
|
|
54
|
+
start: asNumber(range?.start),
|
|
55
|
+
end: asNumber(range?.end),
|
|
56
|
+
text: asString(rec.text),
|
|
57
|
+
};
|
|
58
|
+
if (eff?.bold === true)
|
|
59
|
+
run.bold = true;
|
|
60
|
+
if (eff?.italic === true)
|
|
61
|
+
run.italic = true;
|
|
62
|
+
if (eff?.underline === true)
|
|
63
|
+
run.underline = true;
|
|
64
|
+
if (eff?.strike === true)
|
|
65
|
+
run.strike = true;
|
|
66
|
+
const ff = asString(styles?.fontFamily);
|
|
67
|
+
if (ff)
|
|
68
|
+
run.fontFamily = ff;
|
|
69
|
+
const fs = asNumber(styles?.fontSizePt);
|
|
70
|
+
if (fs > 0)
|
|
71
|
+
run.fontSize = fs;
|
|
72
|
+
const color = asString(styles?.color);
|
|
73
|
+
if (color && color.toLowerCase() !== '#000000')
|
|
74
|
+
run.color = color;
|
|
75
|
+
return run;
|
|
76
|
+
}
|
|
21
77
|
function maybeMethod(api, path) {
|
|
22
78
|
let cursor = api;
|
|
23
79
|
for (const token of path) {
|
|
@@ -54,8 +110,68 @@ async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
54
110
|
const recordError = (section) => (err) => {
|
|
55
111
|
diagnostics.push({ section, message: err instanceof Error ? err.message : String(err) });
|
|
56
112
|
};
|
|
57
|
-
|
|
58
|
-
|
|
113
|
+
let blockOffset = Math.max(0, options.blockOffset ?? 0);
|
|
114
|
+
let blockLimit = options.blockLimit != null && options.blockLimit > 0 ? options.blockLimit : 1000;
|
|
115
|
+
// findText: full-text scan server-side, serialize only ordinals + previews,
|
|
116
|
+
// and (unless the caller pinned blockOffset) center the block window on the
|
|
117
|
+
// first match so one call both LOCATES and READS the target section.
|
|
118
|
+
let finds;
|
|
119
|
+
if (typeof options.findText === 'string' && options.findText.trim().length > 0) {
|
|
120
|
+
const scanFn = maybeMethod(doc, ['blocks', 'list']);
|
|
121
|
+
if (scanFn) {
|
|
122
|
+
// Paginated scan with a hard cap: bounded bridge payloads per page, and
|
|
123
|
+
// an explicit truncated flag instead of a silently-partial `total`.
|
|
124
|
+
const SCAN_PAGE = 2000;
|
|
125
|
+
const SCAN_CAP = 20000;
|
|
126
|
+
const needle = options.findText.toLowerCase();
|
|
127
|
+
const findLimit = options.findLimit != null && options.findLimit > 0 ? options.findLimit : 8;
|
|
128
|
+
const matches = [];
|
|
129
|
+
let scanned = 0;
|
|
130
|
+
let truncated = false;
|
|
131
|
+
for (let pageOffset = 0; pageOffset < SCAN_CAP; pageOffset += SCAN_PAGE) {
|
|
132
|
+
const scanRaw = await safeCall(() => scanFn({ offset: pageOffset, limit: SCAN_PAGE, includeText: true }), null, recordError('blocks.findText'));
|
|
133
|
+
const scanRec = asRecord(scanRaw);
|
|
134
|
+
const scanBlocks = Array.isArray(scanRec?.blocks) ? scanRec.blocks : [];
|
|
135
|
+
scanBlocks.forEach((b, i) => {
|
|
136
|
+
const rec = asRecord(b) ?? {};
|
|
137
|
+
const text = asString(rec.text);
|
|
138
|
+
if (!text || !text.toLowerCase().includes(needle))
|
|
139
|
+
return;
|
|
140
|
+
matches.push({
|
|
141
|
+
ordinal: asNumber(rec.ordinal, pageOffset + i) + 1,
|
|
142
|
+
nodeId: asString(rec.nodeId),
|
|
143
|
+
nodeType: asString(rec.nodeType, 'paragraph'),
|
|
144
|
+
preview: text.slice(0, 100),
|
|
145
|
+
index: asNumber(rec.ordinal, pageOffset + i),
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
scanned += scanBlocks.length;
|
|
149
|
+
const totalBlocksReported = explicitCount(scanRec ?? {}, 'total');
|
|
150
|
+
if (scanBlocks.length < SCAN_PAGE)
|
|
151
|
+
break; // last page
|
|
152
|
+
if (totalBlocksReported != null && scanned >= totalBlocksReported)
|
|
153
|
+
break;
|
|
154
|
+
if (pageOffset + SCAN_PAGE >= SCAN_CAP) {
|
|
155
|
+
truncated = true;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
finds = {
|
|
160
|
+
query: options.findText,
|
|
161
|
+
total: matches.length,
|
|
162
|
+
...(truncated ? { truncated: true } : {}),
|
|
163
|
+
scannedBlocks: scanned,
|
|
164
|
+
matches: matches.slice(0, findLimit).map(({ index: _index, nodeId, ...m }) => ({
|
|
165
|
+
...m,
|
|
166
|
+
...(nodeId ? { nodeId } : {}),
|
|
167
|
+
})),
|
|
168
|
+
};
|
|
169
|
+
if (matches.length > 0 && options.blockOffset == null) {
|
|
170
|
+
blockOffset = Math.max(0, matches[0].index - 2);
|
|
171
|
+
blockLimit = options.blockLimit != null && options.blockLimit > 0 ? options.blockLimit : 12;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
59
175
|
const blockTextLimit = options.blockTextLimit != null && Number.isFinite(options.blockTextLimit) && options.blockTextLimit > 0
|
|
60
176
|
? options.blockTextLimit
|
|
61
177
|
: null;
|
|
@@ -110,6 +226,7 @@ async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
110
226
|
permissionRanges: [],
|
|
111
227
|
images: [],
|
|
112
228
|
diagnostics,
|
|
229
|
+
...(finds ? { finds } : {}),
|
|
113
230
|
};
|
|
114
231
|
}
|
|
115
232
|
// Blocks
|
|
@@ -162,6 +279,35 @@ async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
162
279
|
blocks = blocks.filter((block) => (fullBlockText.get(block) ?? '').trim().length > 0 ||
|
|
163
280
|
(block.nodeType !== 'paragraph' && block.nodeType !== 'listItem'));
|
|
164
281
|
}
|
|
282
|
+
// Optionally enrich body blocks with per-run formatting so a reader can
|
|
283
|
+
// detect a section's intra-paragraph pattern (bold lead-ins, italic terms)
|
|
284
|
+
// and replicate it on an inserted paragraph. Opt-in and bounded — meant to
|
|
285
|
+
// be combined with a narrow blockOffset/blockLimit window. Runs before the
|
|
286
|
+
// dropTextPreview remap so fullBlockText lookups still hit.
|
|
287
|
+
if (options.includeBlockRuns && includeBlocksInSnapshot) {
|
|
288
|
+
const queryFn = maybeMethod(doc, ['query', 'match']);
|
|
289
|
+
if (queryFn) {
|
|
290
|
+
const cap = typeof options.blockRunsLimit === 'number' && options.blockRunsLimit > 0
|
|
291
|
+
? Math.floor(options.blockRunsLimit)
|
|
292
|
+
: 30;
|
|
293
|
+
let enriched = 0;
|
|
294
|
+
for (const block of blocks) {
|
|
295
|
+
if (enriched >= cap)
|
|
296
|
+
break;
|
|
297
|
+
if (block.nodeType !== 'paragraph' && block.nodeType !== 'listItem' && block.nodeType !== 'heading')
|
|
298
|
+
continue;
|
|
299
|
+
// Match on the FULL block text (the snapshot copy may be truncated by
|
|
300
|
+
// blockTextLimit, and a partial pattern would misreport run offsets).
|
|
301
|
+
const text = (fullBlockText.get(block) ?? block.text).trim();
|
|
302
|
+
if (!text || !block.nodeId)
|
|
303
|
+
continue;
|
|
304
|
+
enriched += 1;
|
|
305
|
+
const runs = await safeCall(() => matchRunsForBlock(queryFn, text, block.nodeId), null, recordError('blocks.runs'));
|
|
306
|
+
if (runs && runs.length > 0)
|
|
307
|
+
block.runs = runs;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
165
311
|
if (includeBlocksInSnapshot && options.dropTextPreview === true) {
|
|
166
312
|
blocks = blocks.map((block) => ({ ...block, textPreview: null }));
|
|
167
313
|
}
|
|
@@ -194,6 +340,32 @@ async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
194
340
|
});
|
|
195
341
|
}
|
|
196
342
|
const lists = [...listsById.values()];
|
|
343
|
+
// Optionally enrich list items with per-run formatting so a reader can detect
|
|
344
|
+
// and replicate an intra-item pattern. Costs one query.match per item, so it
|
|
345
|
+
// is opt-in and bounded. The block's runs tile the matched text in offset
|
|
346
|
+
// order; we match by the item's own nodeId to disambiguate duplicate text.
|
|
347
|
+
if (options.includeListItemRuns && includesDomain(requestedDomains, 'lists')) {
|
|
348
|
+
const queryFn = maybeMethod(doc, ['query', 'match']);
|
|
349
|
+
if (queryFn) {
|
|
350
|
+
const cap = typeof options.listItemRunsLimit === 'number' && options.listItemRunsLimit > 0
|
|
351
|
+
? Math.floor(options.listItemRunsLimit)
|
|
352
|
+
: 40;
|
|
353
|
+
let enriched = 0;
|
|
354
|
+
outer: for (const list of listsById.values()) {
|
|
355
|
+
for (const item of list.items) {
|
|
356
|
+
if (enriched >= cap)
|
|
357
|
+
break outer;
|
|
358
|
+
const text = item.text?.trim();
|
|
359
|
+
if (!text || !item.nodeId)
|
|
360
|
+
continue;
|
|
361
|
+
enriched += 1;
|
|
362
|
+
const runs = await safeCall(() => matchRunsForBlock(queryFn, text, item.nodeId), null, recordError('lists.itemRuns'));
|
|
363
|
+
if (runs && runs.length > 0)
|
|
364
|
+
item.runs = runs;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
197
369
|
// Tables: derive table block list from `blocks`, then query shape per table.
|
|
198
370
|
const tableBlocks = normalizedBlocks.filter((b) => b.nodeType === 'table');
|
|
199
371
|
const precedingBlocksRaw = blockOffset > 0 && includesDomain(requestedDomains, 'tables') && tableBlocks.length > 0 && blocksFn
|
|
@@ -273,6 +445,30 @@ async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
273
445
|
table.cells = [...cellMap.values()].sort((left, right) => left.rowIndex - right.rowIndex || left.columnIndex - right.columnIndex);
|
|
274
446
|
}
|
|
275
447
|
}
|
|
448
|
+
// Optionally enrich table cells with per-run formatting (opt-in; one
|
|
449
|
+
// query.match per cell, bounded) so a reader can match a cell's pattern.
|
|
450
|
+
if (options.includeTableCellRuns && includesDomain(requestedDomains, 'tables')) {
|
|
451
|
+
const queryFn = maybeMethod(doc, ['query', 'match']);
|
|
452
|
+
if (queryFn) {
|
|
453
|
+
const cap = typeof options.tableCellRunsLimit === 'number' && options.tableCellRunsLimit > 0
|
|
454
|
+
? Math.floor(options.tableCellRunsLimit)
|
|
455
|
+
: 60;
|
|
456
|
+
let enriched = 0;
|
|
457
|
+
outer: for (const table of tables) {
|
|
458
|
+
for (const cell of table.cells) {
|
|
459
|
+
if (enriched >= cap)
|
|
460
|
+
break outer;
|
|
461
|
+
const text = cell.text?.trim();
|
|
462
|
+
if (!text || !cell.nodeId)
|
|
463
|
+
continue;
|
|
464
|
+
enriched += 1;
|
|
465
|
+
const runs = await safeCall(() => matchRunsForBlock(queryFn, text, cell.nodeId), null, recordError('tables.cellRuns'));
|
|
466
|
+
if (runs && runs.length > 0)
|
|
467
|
+
cell.runs = runs;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
276
472
|
// Comments
|
|
277
473
|
const commentsFn = maybeMethod(doc, ['comments', 'list']);
|
|
278
474
|
const commentsRaw = includesDomain(requestedDomains, 'comments') && commentsFn
|
|
@@ -549,6 +745,7 @@ async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
549
745
|
permissionRanges,
|
|
550
746
|
images,
|
|
551
747
|
diagnostics,
|
|
748
|
+
...(finds ? { finds } : {}),
|
|
552
749
|
};
|
|
553
750
|
}
|
|
554
751
|
/**
|
|
@@ -686,4 +883,5 @@ function resolveSnapshotSelector(snapshot, selector) {
|
|
|
686
883
|
|
|
687
884
|
exports.AmbiguousSelectorError = AmbiguousSelectorError;
|
|
688
885
|
exports.buildDocumentSnapshot = buildDocumentSnapshot;
|
|
886
|
+
exports.matchRunsForBlock = matchRunsForBlock;
|
|
689
887
|
exports.resolveSnapshotSelector = resolveSnapshotSelector;
|
|
@@ -38,6 +38,29 @@ export type SnapshotBlock = {
|
|
|
38
38
|
path: number[] | null;
|
|
39
39
|
kind: string | null;
|
|
40
40
|
} | null;
|
|
41
|
+
/** Per-run formatting, present only when inspect was asked to include it. */
|
|
42
|
+
runs?: ReadonlyArray<SnapshotRun>;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* One inline run within a list item, in offset order (runs tile the item text).
|
|
46
|
+
* Marks are the EFFECTIVE (resolved) state — what the run visually IS — so the
|
|
47
|
+
* agent can read a formatting pattern across sibling items (e.g. "first word
|
|
48
|
+
* plain, second word italic") and reproduce it on a new item. Boolean marks are
|
|
49
|
+
* emitted only when true to keep the payload compact; a run with no marks is
|
|
50
|
+
* plain. Sourced from query.match's per-run style projection.
|
|
51
|
+
*/
|
|
52
|
+
export type SnapshotRun = {
|
|
53
|
+
start: number;
|
|
54
|
+
end: number;
|
|
55
|
+
text: string;
|
|
56
|
+
bold?: boolean;
|
|
57
|
+
italic?: boolean;
|
|
58
|
+
underline?: boolean;
|
|
59
|
+
strike?: boolean;
|
|
60
|
+
fontFamily?: string;
|
|
61
|
+
/** Point size. */
|
|
62
|
+
fontSize?: number;
|
|
63
|
+
color?: string;
|
|
41
64
|
};
|
|
42
65
|
export type SnapshotList = {
|
|
43
66
|
listId: string;
|
|
@@ -47,6 +70,8 @@ export type SnapshotList = {
|
|
|
47
70
|
ordinal: number;
|
|
48
71
|
level: number;
|
|
49
72
|
text: string;
|
|
73
|
+
/** Per-run formatting, present only when inspect was asked to include it. */
|
|
74
|
+
runs?: ReadonlyArray<SnapshotRun>;
|
|
50
75
|
}>;
|
|
51
76
|
};
|
|
52
77
|
export type SnapshotTable = {
|
|
@@ -59,6 +84,8 @@ export type SnapshotTable = {
|
|
|
59
84
|
columnIndex: number;
|
|
60
85
|
text: string;
|
|
61
86
|
nodeId?: string;
|
|
87
|
+
/** Per-run formatting, present only when inspect was asked to include it. */
|
|
88
|
+
runs?: ReadonlyArray<SnapshotRun>;
|
|
62
89
|
}>;
|
|
63
90
|
};
|
|
64
91
|
export type SnapshotComment = {
|
|
@@ -180,7 +207,30 @@ export type DocumentSnapshot = {
|
|
|
180
207
|
permissionRanges: readonly SnapshotPermissionRange[];
|
|
181
208
|
images: readonly SnapshotImage[];
|
|
182
209
|
diagnostics: readonly SnapshotDiagnostic[];
|
|
210
|
+
/** Present when the snapshot was built with `findText` — see SnapshotOptions. */
|
|
211
|
+
finds?: {
|
|
212
|
+
query: string;
|
|
213
|
+
/** Matches within the scanned range; see truncated/scannedBlocks. */
|
|
214
|
+
total: number;
|
|
215
|
+
/** True when the document has more blocks than the scan cap covered. */
|
|
216
|
+
truncated?: boolean;
|
|
217
|
+
scannedBlocks?: number;
|
|
218
|
+
matches: ReadonlyArray<{
|
|
219
|
+
ordinal: number;
|
|
220
|
+
nodeId?: string;
|
|
221
|
+
nodeType: string;
|
|
222
|
+
preview: string;
|
|
223
|
+
}>;
|
|
224
|
+
};
|
|
183
225
|
};
|
|
226
|
+
/**
|
|
227
|
+
* Read a specific block's per-run formatting via query.match, matching STRICTLY
|
|
228
|
+
* by nodeId. When the text is ambiguous (multiple matches) and none matches the
|
|
229
|
+
* requested nodeId, returns [] rather than guessing a different block — so a
|
|
230
|
+
* duplicate-text sibling can never contribute the wrong runs. The single-match
|
|
231
|
+
* fallback is used only when there is exactly one candidate (unambiguous).
|
|
232
|
+
*/
|
|
233
|
+
export declare function matchRunsForBlock(queryFn: (args: Record<string, unknown>) => Promise<unknown>, text: string, nodeId: string): Promise<SnapshotRun[]>;
|
|
184
234
|
type SnapshotOptions = {
|
|
185
235
|
countsOnly?: boolean;
|
|
186
236
|
includeDomains?: readonly SnapshotDomain[];
|
|
@@ -201,6 +251,17 @@ type SnapshotOptions = {
|
|
|
201
251
|
*/
|
|
202
252
|
blockOffset?: number;
|
|
203
253
|
blockLimit?: number;
|
|
254
|
+
/**
|
|
255
|
+
* Locate blocks whose text contains this string (case-insensitive) BEFORE
|
|
256
|
+
* windowing. The scan runs server-side over the full block list; only the
|
|
257
|
+
* match ordinals + short previews serialize back (snapshot.finds), and when
|
|
258
|
+
* the caller did not pin blockOffset the block window auto-centers on the
|
|
259
|
+
* first match. This is the section-jump primitive for large documents —
|
|
260
|
+
* one findText inspect replaces a linear sweep of wide windows.
|
|
261
|
+
*/
|
|
262
|
+
findText?: string;
|
|
263
|
+
/** Cap on reported findText matches (default 8). */
|
|
264
|
+
findLimit?: number;
|
|
204
265
|
/**
|
|
205
266
|
* Drop empty paragraphs/list-items from the returned blocks. A real DOCX is
|
|
206
267
|
* ~40% empty spacer paragraphs that carry full JSON overhead but no reading
|
|
@@ -214,6 +275,36 @@ type SnapshotOptions = {
|
|
|
214
275
|
* `text`.
|
|
215
276
|
*/
|
|
216
277
|
dropTextPreview?: boolean;
|
|
278
|
+
/**
|
|
279
|
+
* Attach per-run formatting (`items[].runs`) to the returned list items, so a
|
|
280
|
+
* reader can detect an intra-item formatting pattern (which segment is bold/
|
|
281
|
+
* italic, the font/size) and replicate it on a new item. Off by default: it
|
|
282
|
+
* costs one query.match per list item, so only request it when matching a
|
|
283
|
+
* list's formatting matters. Bounded by `listItemRunsLimit`.
|
|
284
|
+
*/
|
|
285
|
+
includeListItemRuns?: boolean;
|
|
286
|
+
/** Max list items to enrich with runs when includeListItemRuns is set (default 40). */
|
|
287
|
+
listItemRunsLimit?: number;
|
|
288
|
+
/**
|
|
289
|
+
* Attach per-run formatting (`cells[].runs`) to returned table cells, so a
|
|
290
|
+
* reader can match a cell's formatting pattern. OFF by default: tables can be
|
|
291
|
+
* large and it costs one query.match per cell, so only request it when cell
|
|
292
|
+
* styling actually matters. Bounded by `tableCellRunsLimit`.
|
|
293
|
+
*/
|
|
294
|
+
includeTableCellRuns?: boolean;
|
|
295
|
+
/** Max table cells to enrich with runs when includeTableCellRuns is set (default 60). */
|
|
296
|
+
tableCellRunsLimit?: number;
|
|
297
|
+
/**
|
|
298
|
+
* Attach per-run formatting (`blocks[].runs`) to the returned body blocks so
|
|
299
|
+
* a reader can detect a section's formatting pattern (e.g. lettered clauses
|
|
300
|
+
* with a bold "(m)" lead-in) and replicate it on an inserted paragraph. OFF
|
|
301
|
+
* by default: it costs one query.match per block, so request it with a
|
|
302
|
+
* narrow blockOffset/blockLimit window around the insertion target. Bounded
|
|
303
|
+
* by `blockRunsLimit`.
|
|
304
|
+
*/
|
|
305
|
+
includeBlockRuns?: boolean;
|
|
306
|
+
/** Max blocks to enrich with runs when includeBlockRuns is set (default 30). */
|
|
307
|
+
blockRunsLimit?: number;
|
|
217
308
|
};
|
|
218
309
|
/**
|
|
219
310
|
* Build a deterministic snapshot of a document. The snapshot uses only
|
|
@@ -16,6 +16,62 @@ async function safeCall(thunk, fallback, onError) {
|
|
|
16
16
|
return fallback;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Read a specific block's per-run formatting via query.match, matching STRICTLY
|
|
21
|
+
* by nodeId. When the text is ambiguous (multiple matches) and none matches the
|
|
22
|
+
* requested nodeId, returns [] rather than guessing a different block — so a
|
|
23
|
+
* duplicate-text sibling can never contribute the wrong runs. The single-match
|
|
24
|
+
* fallback is used only when there is exactly one candidate (unambiguous).
|
|
25
|
+
*/
|
|
26
|
+
export async function matchRunsForBlock(queryFn, text, nodeId) {
|
|
27
|
+
const res = asRecord(await queryFn({ select: { type: 'text', pattern: text } }));
|
|
28
|
+
const items = (Array.isArray(res?.items) ? res.items : []).map(asRecord);
|
|
29
|
+
const match = items.find((m) => asString(asRecord(m?.address)?.nodeId) === nodeId) ?? (items.length === 1 ? items[0] : null);
|
|
30
|
+
if (!match)
|
|
31
|
+
return [];
|
|
32
|
+
const blocksArr = Array.isArray(match.blocks) ? match.blocks.map(asRecord) : [];
|
|
33
|
+
const block = blocksArr.find((b) => asString(b?.blockId) === nodeId) ?? (blocksArr.length === 1 ? blocksArr[0] : null);
|
|
34
|
+
if (!block)
|
|
35
|
+
return [];
|
|
36
|
+
const rawRuns = Array.isArray(block.runs) ? block.runs : [];
|
|
37
|
+
return rawRuns.map(projectRun).filter((r) => r != null);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Project one raw query.match run into a compact SnapshotRun. Uses the
|
|
41
|
+
* EFFECTIVE (resolved) mark state and emits booleans only when true. Skips a
|
|
42
|
+
* default-black color so only meaningful colors surface.
|
|
43
|
+
*/
|
|
44
|
+
function projectRun(raw) {
|
|
45
|
+
const rec = asRecord(raw);
|
|
46
|
+
if (!rec)
|
|
47
|
+
return null;
|
|
48
|
+
const range = asRecord(rec.range);
|
|
49
|
+
const styles = asRecord(rec.styles);
|
|
50
|
+
const eff = asRecord(styles?.effective);
|
|
51
|
+
const run = {
|
|
52
|
+
start: asNumber(range?.start),
|
|
53
|
+
end: asNumber(range?.end),
|
|
54
|
+
text: asString(rec.text),
|
|
55
|
+
};
|
|
56
|
+
if (eff?.bold === true)
|
|
57
|
+
run.bold = true;
|
|
58
|
+
if (eff?.italic === true)
|
|
59
|
+
run.italic = true;
|
|
60
|
+
if (eff?.underline === true)
|
|
61
|
+
run.underline = true;
|
|
62
|
+
if (eff?.strike === true)
|
|
63
|
+
run.strike = true;
|
|
64
|
+
const ff = asString(styles?.fontFamily);
|
|
65
|
+
if (ff)
|
|
66
|
+
run.fontFamily = ff;
|
|
67
|
+
const fs = asNumber(styles?.fontSizePt);
|
|
68
|
+
if (fs > 0)
|
|
69
|
+
run.fontSize = fs;
|
|
70
|
+
const color = asString(styles?.color);
|
|
71
|
+
if (color && color.toLowerCase() !== '#000000')
|
|
72
|
+
run.color = color;
|
|
73
|
+
return run;
|
|
74
|
+
}
|
|
19
75
|
function maybeMethod(api, path) {
|
|
20
76
|
let cursor = api;
|
|
21
77
|
for (const token of path) {
|
|
@@ -52,8 +108,68 @@ export async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
52
108
|
const recordError = (section) => (err) => {
|
|
53
109
|
diagnostics.push({ section, message: err instanceof Error ? err.message : String(err) });
|
|
54
110
|
};
|
|
55
|
-
|
|
56
|
-
|
|
111
|
+
let blockOffset = Math.max(0, options.blockOffset ?? 0);
|
|
112
|
+
let blockLimit = options.blockLimit != null && options.blockLimit > 0 ? options.blockLimit : 1000;
|
|
113
|
+
// findText: full-text scan server-side, serialize only ordinals + previews,
|
|
114
|
+
// and (unless the caller pinned blockOffset) center the block window on the
|
|
115
|
+
// first match so one call both LOCATES and READS the target section.
|
|
116
|
+
let finds;
|
|
117
|
+
if (typeof options.findText === 'string' && options.findText.trim().length > 0) {
|
|
118
|
+
const scanFn = maybeMethod(doc, ['blocks', 'list']);
|
|
119
|
+
if (scanFn) {
|
|
120
|
+
// Paginated scan with a hard cap: bounded bridge payloads per page, and
|
|
121
|
+
// an explicit truncated flag instead of a silently-partial `total`.
|
|
122
|
+
const SCAN_PAGE = 2000;
|
|
123
|
+
const SCAN_CAP = 20000;
|
|
124
|
+
const needle = options.findText.toLowerCase();
|
|
125
|
+
const findLimit = options.findLimit != null && options.findLimit > 0 ? options.findLimit : 8;
|
|
126
|
+
const matches = [];
|
|
127
|
+
let scanned = 0;
|
|
128
|
+
let truncated = false;
|
|
129
|
+
for (let pageOffset = 0; pageOffset < SCAN_CAP; pageOffset += SCAN_PAGE) {
|
|
130
|
+
const scanRaw = await safeCall(() => scanFn({ offset: pageOffset, limit: SCAN_PAGE, includeText: true }), null, recordError('blocks.findText'));
|
|
131
|
+
const scanRec = asRecord(scanRaw);
|
|
132
|
+
const scanBlocks = Array.isArray(scanRec?.blocks) ? scanRec.blocks : [];
|
|
133
|
+
scanBlocks.forEach((b, i) => {
|
|
134
|
+
const rec = asRecord(b) ?? {};
|
|
135
|
+
const text = asString(rec.text);
|
|
136
|
+
if (!text || !text.toLowerCase().includes(needle))
|
|
137
|
+
return;
|
|
138
|
+
matches.push({
|
|
139
|
+
ordinal: asNumber(rec.ordinal, pageOffset + i) + 1,
|
|
140
|
+
nodeId: asString(rec.nodeId),
|
|
141
|
+
nodeType: asString(rec.nodeType, 'paragraph'),
|
|
142
|
+
preview: text.slice(0, 100),
|
|
143
|
+
index: asNumber(rec.ordinal, pageOffset + i),
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
scanned += scanBlocks.length;
|
|
147
|
+
const totalBlocksReported = explicitCount(scanRec ?? {}, 'total');
|
|
148
|
+
if (scanBlocks.length < SCAN_PAGE)
|
|
149
|
+
break; // last page
|
|
150
|
+
if (totalBlocksReported != null && scanned >= totalBlocksReported)
|
|
151
|
+
break;
|
|
152
|
+
if (pageOffset + SCAN_PAGE >= SCAN_CAP) {
|
|
153
|
+
truncated = true;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
finds = {
|
|
158
|
+
query: options.findText,
|
|
159
|
+
total: matches.length,
|
|
160
|
+
...(truncated ? { truncated: true } : {}),
|
|
161
|
+
scannedBlocks: scanned,
|
|
162
|
+
matches: matches.slice(0, findLimit).map(({ index: _index, nodeId, ...m }) => ({
|
|
163
|
+
...m,
|
|
164
|
+
...(nodeId ? { nodeId } : {}),
|
|
165
|
+
})),
|
|
166
|
+
};
|
|
167
|
+
if (matches.length > 0 && options.blockOffset == null) {
|
|
168
|
+
blockOffset = Math.max(0, matches[0].index - 2);
|
|
169
|
+
blockLimit = options.blockLimit != null && options.blockLimit > 0 ? options.blockLimit : 12;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
57
173
|
const blockTextLimit = options.blockTextLimit != null && Number.isFinite(options.blockTextLimit) && options.blockTextLimit > 0
|
|
58
174
|
? options.blockTextLimit
|
|
59
175
|
: null;
|
|
@@ -108,6 +224,7 @@ export async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
108
224
|
permissionRanges: [],
|
|
109
225
|
images: [],
|
|
110
226
|
diagnostics,
|
|
227
|
+
...(finds ? { finds } : {}),
|
|
111
228
|
};
|
|
112
229
|
}
|
|
113
230
|
// Blocks
|
|
@@ -160,6 +277,35 @@ export async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
160
277
|
blocks = blocks.filter((block) => (fullBlockText.get(block) ?? '').trim().length > 0 ||
|
|
161
278
|
(block.nodeType !== 'paragraph' && block.nodeType !== 'listItem'));
|
|
162
279
|
}
|
|
280
|
+
// Optionally enrich body blocks with per-run formatting so a reader can
|
|
281
|
+
// detect a section's intra-paragraph pattern (bold lead-ins, italic terms)
|
|
282
|
+
// and replicate it on an inserted paragraph. Opt-in and bounded — meant to
|
|
283
|
+
// be combined with a narrow blockOffset/blockLimit window. Runs before the
|
|
284
|
+
// dropTextPreview remap so fullBlockText lookups still hit.
|
|
285
|
+
if (options.includeBlockRuns && includeBlocksInSnapshot) {
|
|
286
|
+
const queryFn = maybeMethod(doc, ['query', 'match']);
|
|
287
|
+
if (queryFn) {
|
|
288
|
+
const cap = typeof options.blockRunsLimit === 'number' && options.blockRunsLimit > 0
|
|
289
|
+
? Math.floor(options.blockRunsLimit)
|
|
290
|
+
: 30;
|
|
291
|
+
let enriched = 0;
|
|
292
|
+
for (const block of blocks) {
|
|
293
|
+
if (enriched >= cap)
|
|
294
|
+
break;
|
|
295
|
+
if (block.nodeType !== 'paragraph' && block.nodeType !== 'listItem' && block.nodeType !== 'heading')
|
|
296
|
+
continue;
|
|
297
|
+
// Match on the FULL block text (the snapshot copy may be truncated by
|
|
298
|
+
// blockTextLimit, and a partial pattern would misreport run offsets).
|
|
299
|
+
const text = (fullBlockText.get(block) ?? block.text).trim();
|
|
300
|
+
if (!text || !block.nodeId)
|
|
301
|
+
continue;
|
|
302
|
+
enriched += 1;
|
|
303
|
+
const runs = await safeCall(() => matchRunsForBlock(queryFn, text, block.nodeId), null, recordError('blocks.runs'));
|
|
304
|
+
if (runs && runs.length > 0)
|
|
305
|
+
block.runs = runs;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
163
309
|
if (includeBlocksInSnapshot && options.dropTextPreview === true) {
|
|
164
310
|
blocks = blocks.map((block) => ({ ...block, textPreview: null }));
|
|
165
311
|
}
|
|
@@ -192,6 +338,32 @@ export async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
192
338
|
});
|
|
193
339
|
}
|
|
194
340
|
const lists = [...listsById.values()];
|
|
341
|
+
// Optionally enrich list items with per-run formatting so a reader can detect
|
|
342
|
+
// and replicate an intra-item pattern. Costs one query.match per item, so it
|
|
343
|
+
// is opt-in and bounded. The block's runs tile the matched text in offset
|
|
344
|
+
// order; we match by the item's own nodeId to disambiguate duplicate text.
|
|
345
|
+
if (options.includeListItemRuns && includesDomain(requestedDomains, 'lists')) {
|
|
346
|
+
const queryFn = maybeMethod(doc, ['query', 'match']);
|
|
347
|
+
if (queryFn) {
|
|
348
|
+
const cap = typeof options.listItemRunsLimit === 'number' && options.listItemRunsLimit > 0
|
|
349
|
+
? Math.floor(options.listItemRunsLimit)
|
|
350
|
+
: 40;
|
|
351
|
+
let enriched = 0;
|
|
352
|
+
outer: for (const list of listsById.values()) {
|
|
353
|
+
for (const item of list.items) {
|
|
354
|
+
if (enriched >= cap)
|
|
355
|
+
break outer;
|
|
356
|
+
const text = item.text?.trim();
|
|
357
|
+
if (!text || !item.nodeId)
|
|
358
|
+
continue;
|
|
359
|
+
enriched += 1;
|
|
360
|
+
const runs = await safeCall(() => matchRunsForBlock(queryFn, text, item.nodeId), null, recordError('lists.itemRuns'));
|
|
361
|
+
if (runs && runs.length > 0)
|
|
362
|
+
item.runs = runs;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
195
367
|
// Tables: derive table block list from `blocks`, then query shape per table.
|
|
196
368
|
const tableBlocks = normalizedBlocks.filter((b) => b.nodeType === 'table');
|
|
197
369
|
const precedingBlocksRaw = blockOffset > 0 && includesDomain(requestedDomains, 'tables') && tableBlocks.length > 0 && blocksFn
|
|
@@ -271,6 +443,30 @@ export async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
271
443
|
table.cells = [...cellMap.values()].sort((left, right) => left.rowIndex - right.rowIndex || left.columnIndex - right.columnIndex);
|
|
272
444
|
}
|
|
273
445
|
}
|
|
446
|
+
// Optionally enrich table cells with per-run formatting (opt-in; one
|
|
447
|
+
// query.match per cell, bounded) so a reader can match a cell's pattern.
|
|
448
|
+
if (options.includeTableCellRuns && includesDomain(requestedDomains, 'tables')) {
|
|
449
|
+
const queryFn = maybeMethod(doc, ['query', 'match']);
|
|
450
|
+
if (queryFn) {
|
|
451
|
+
const cap = typeof options.tableCellRunsLimit === 'number' && options.tableCellRunsLimit > 0
|
|
452
|
+
? Math.floor(options.tableCellRunsLimit)
|
|
453
|
+
: 60;
|
|
454
|
+
let enriched = 0;
|
|
455
|
+
outer: for (const table of tables) {
|
|
456
|
+
for (const cell of table.cells) {
|
|
457
|
+
if (enriched >= cap)
|
|
458
|
+
break outer;
|
|
459
|
+
const text = cell.text?.trim();
|
|
460
|
+
if (!text || !cell.nodeId)
|
|
461
|
+
continue;
|
|
462
|
+
enriched += 1;
|
|
463
|
+
const runs = await safeCall(() => matchRunsForBlock(queryFn, text, cell.nodeId), null, recordError('tables.cellRuns'));
|
|
464
|
+
if (runs && runs.length > 0)
|
|
465
|
+
cell.runs = runs;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
274
470
|
// Comments
|
|
275
471
|
const commentsFn = maybeMethod(doc, ['comments', 'list']);
|
|
276
472
|
const commentsRaw = includesDomain(requestedDomains, 'comments') && commentsFn
|
|
@@ -547,6 +743,7 @@ export async function buildDocumentSnapshot(doc, options = {}) {
|
|
|
547
743
|
permissionRanges,
|
|
548
744
|
images,
|
|
549
745
|
diagnostics,
|
|
746
|
+
...(finds ? { finds } : {}),
|
|
550
747
|
};
|
|
551
748
|
}
|
|
552
749
|
/**
|
package/dist/agent/runtime.cjs
CHANGED
|
@@ -153,7 +153,15 @@ function resolveDocMethod(doc, operationId) {
|
|
|
153
153
|
return cursor;
|
|
154
154
|
}
|
|
155
155
|
async function agentInspect(doc, args = {}) {
|
|
156
|
-
|
|
156
|
+
// For the MODEL-facing inspect, expose each list item's per-run formatting BY
|
|
157
|
+
// DEFAULT so the agent always sees the intra-item pattern it must match when
|
|
158
|
+
// adding items. (Internal pre/post snapshots call buildDocumentSnapshot
|
|
159
|
+
// directly and stay cheap — this default lives only on the tool path.) Opt out
|
|
160
|
+
// with includeListItemRuns:false; countsOnly skips all domains anyway.
|
|
161
|
+
const listsInScope = !args.countsOnly &&
|
|
162
|
+
(!Array.isArray(args.includeDomains) || args.includeDomains.length === 0 || args.includeDomains.includes('lists'));
|
|
163
|
+
const effective = args.includeListItemRuns === undefined && listsInScope ? { ...args, includeListItemRuns: true } : args;
|
|
164
|
+
return docSnapshot.buildDocumentSnapshot(doc, effective);
|
|
157
165
|
}
|
|
158
166
|
function checkAgainstSnapshot(snapshot, check) {
|
|
159
167
|
switch (check.kind) {
|
package/dist/agent/runtime.d.ts
CHANGED
|
@@ -31,8 +31,16 @@ export type AgentInspectArgs = {
|
|
|
31
31
|
trackedChangeLimit?: number;
|
|
32
32
|
blockOffset?: number;
|
|
33
33
|
blockLimit?: number;
|
|
34
|
+
findText?: string;
|
|
35
|
+
findLimit?: number;
|
|
34
36
|
omitEmptyBlocks?: boolean;
|
|
35
37
|
dropTextPreview?: boolean;
|
|
38
|
+
includeListItemRuns?: boolean;
|
|
39
|
+
listItemRunsLimit?: number;
|
|
40
|
+
includeTableCellRuns?: boolean;
|
|
41
|
+
tableCellRunsLimit?: number;
|
|
42
|
+
includeBlockRuns?: boolean;
|
|
43
|
+
blockRunsLimit?: number;
|
|
36
44
|
};
|
|
37
45
|
export type AgentApplyArgs = {
|
|
38
46
|
plan: AgentPlan;
|