@superdoc-dev/sdk 1.19.2 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action-primitives/doc-index.cjs +215 -0
- package/dist/action-primitives/doc-index.d.ts +83 -0
- package/dist/action-primitives/doc-index.d.ts.map +1 -0
- package/dist/action-primitives/doc-index.js +211 -0
- package/dist/action-primitives/engine.cjs +204 -0
- package/dist/action-primitives/engine.d.ts +71 -0
- package/dist/action-primitives/engine.d.ts.map +1 -0
- package/dist/action-primitives/engine.js +196 -0
- package/dist/action-primitives/receipt.cjs +39 -0
- package/dist/action-primitives/receipt.d.ts +49 -0
- package/dist/action-primitives/receipt.d.ts.map +1 -0
- package/dist/action-primitives/receipt.js +32 -0
- package/dist/action-primitives/resolve.cjs +252 -0
- package/dist/action-primitives/resolve.d.ts +56 -0
- package/dist/action-primitives/resolve.d.ts.map +1 -0
- package/dist/action-primitives/resolve.js +246 -0
- package/dist/action-primitives/session-cache.cjs +43 -0
- package/dist/action-primitives/session-cache.d.ts +19 -0
- package/dist/action-primitives/session-cache.d.ts.map +1 -0
- package/dist/action-primitives/session-cache.js +37 -0
- package/dist/action-primitives/tools/list-transform.cjs +661 -0
- package/dist/action-primitives/tools/list-transform.d.ts +98 -0
- package/dist/action-primitives/tools/list-transform.d.ts.map +1 -0
- package/dist/action-primitives/tools/list-transform.js +656 -0
- package/dist/action-primitives/tools/structure-insert.cjs +1343 -0
- package/dist/action-primitives/tools/structure-insert.d.ts +183 -0
- package/dist/action-primitives/tools/structure-insert.d.ts.map +1 -0
- package/dist/action-primitives/tools/structure-insert.js +1338 -0
- package/dist/action-primitives/tools/text-transform.cjs +669 -0
- package/dist/action-primitives/tools/text-transform.d.ts +64 -0
- package/dist/action-primitives/tools/text-transform.d.ts.map +1 -0
- package/dist/action-primitives/tools/text-transform.js +664 -0
- package/dist/action-primitives/types.d.ts +36 -0
- package/dist/action-primitives/types.d.ts.map +1 -0
- package/dist/action-primitives/types.js +15 -0
- package/dist/agent/actions.cjs +5381 -0
- package/dist/agent/actions.d.ts +404 -0
- package/dist/agent/actions.d.ts.map +1 -0
- package/dist/agent/actions.js +5373 -0
- package/dist/agent/catalog.cjs +483 -0
- package/dist/agent/catalog.d.ts +103 -0
- package/dist/agent/catalog.d.ts.map +1 -0
- package/dist/agent/catalog.js +471 -0
- package/dist/agent/doc-snapshot.cjs +663 -0
- package/dist/agent/doc-snapshot.d.ts +247 -0
- package/dist/agent/doc-snapshot.d.ts.map +1 -0
- package/dist/agent/doc-snapshot.js +657 -0
- package/dist/agent/index.d.ts +16 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +15 -0
- package/dist/agent/ir.cjs +170 -0
- package/dist/agent/ir.d.ts +216 -0
- package/dist/agent/ir.d.ts.map +1 -0
- package/dist/agent/ir.js +181 -0
- package/dist/agent/operation-catalog.cjs +418 -0
- package/dist/agent/operation-catalog.d.ts +36 -0
- package/dist/agent/operation-catalog.d.ts.map +1 -0
- package/dist/agent/operation-catalog.js +446 -0
- package/dist/agent/runtime.cjs +501 -0
- package/dist/agent/runtime.d.ts +120 -0
- package/dist/agent/runtime.d.ts.map +1 -0
- package/dist/agent/runtime.js +493 -0
- package/dist/embedded-prompts.generated.cjs +13 -0
- package/dist/embedded-prompts.generated.d.ts +4 -0
- package/dist/embedded-prompts.generated.d.ts.map +1 -0
- package/dist/embedded-prompts.generated.js +9 -0
- package/dist/generated/client.cjs +18 -0
- package/dist/generated/client.d.ts +134 -0
- package/dist/generated/client.d.ts.map +1 -1
- package/dist/generated/client.js +18 -0
- package/dist/generated/contract.cjs +622 -3
- package/dist/generated/contract.d.ts.map +1 -1
- package/dist/generated/contract.js +622 -3
- package/dist/index.cjs +3 -7
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -8
- package/dist/presets/core.cjs +454 -0
- package/dist/presets/core.d.ts +20 -0
- package/dist/presets/core.d.ts.map +1 -0
- package/dist/presets/core.js +447 -0
- package/dist/presets.cjs +55 -8
- package/dist/presets.d.ts +39 -8
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +53 -8
- package/dist/prompts/mcp-prompt.md +23 -0
- package/dist/prompts/system-prompt.md +108 -0
- package/dist/runtime/transport-common.cjs +8 -0
- package/dist/runtime/transport-common.d.ts.map +1 -1
- package/dist/runtime/transport-common.js +8 -0
- package/dist/tools.cjs +46 -11
- package/dist/tools.d.ts +55 -8
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +45 -13
- package/package.json +9 -8
- package/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- package/tools/__pycache__/intent_dispatch_generated.cpython-312.pyc +0 -0
- package/tools/catalog.json +14 -0
- package/tools/tools-policy.json +1 -1
- package/tools/tools.anthropic.json +14 -0
- package/tools/tools.generic.json +14 -0
- package/tools/tools.openai.json +14 -0
- package/tools/tools.vercel.json +14 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var contract = require('../generated/contract.cjs');
|
|
4
|
+
var errors = require('../runtime/errors.cjs');
|
|
5
|
+
var ir = require('./ir.cjs');
|
|
6
|
+
var docSnapshot = require('./doc-snapshot.cjs');
|
|
7
|
+
var operationCatalog = require('./operation-catalog.cjs');
|
|
8
|
+
|
|
9
|
+
const RESERVED_ARG_KEYS = new Set(['sessionId', 'doc']);
|
|
10
|
+
function ensureKnownOperation(operationId) {
|
|
11
|
+
const entry = operationCatalog.getOperationCatalogEntry(operationId);
|
|
12
|
+
if (!entry) {
|
|
13
|
+
throw new errors.SuperDocCliError(`Unknown operation: ${operationId}`, {
|
|
14
|
+
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
15
|
+
details: { operationId },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return entry;
|
|
19
|
+
}
|
|
20
|
+
function ensureClean(args) {
|
|
21
|
+
for (const key of Object.keys(args)) {
|
|
22
|
+
if (RESERVED_ARG_KEYS.has(key)) {
|
|
23
|
+
throw new errors.SuperDocCliError(`Reserved key "${key}" must not appear in agent args.`, {
|
|
24
|
+
code: 'INVALID_ARGUMENT',
|
|
25
|
+
details: { key },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return args;
|
|
30
|
+
}
|
|
31
|
+
function isRecord(value) {
|
|
32
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function isRefToken(value) {
|
|
35
|
+
return (isRecord(value) &&
|
|
36
|
+
typeof value.ref === 'string' &&
|
|
37
|
+
(value.path == null || typeof value.path === 'string') &&
|
|
38
|
+
Object.keys(value).every((key) => key === 'ref' || key === 'path'));
|
|
39
|
+
}
|
|
40
|
+
function readBindingPath(value, path) {
|
|
41
|
+
if (!path)
|
|
42
|
+
return value;
|
|
43
|
+
let cursor = value;
|
|
44
|
+
for (const segment of path.split('.').filter((part) => part.length > 0)) {
|
|
45
|
+
if (Array.isArray(cursor)) {
|
|
46
|
+
const index = Number(segment);
|
|
47
|
+
if (!Number.isInteger(index) || index < 0 || index >= cursor.length)
|
|
48
|
+
return undefined;
|
|
49
|
+
cursor = cursor[index];
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!isRecord(cursor) || !(segment in cursor))
|
|
53
|
+
return undefined;
|
|
54
|
+
cursor = cursor[segment];
|
|
55
|
+
}
|
|
56
|
+
return cursor;
|
|
57
|
+
}
|
|
58
|
+
function resolveBindingRef(bindings, ref, path) {
|
|
59
|
+
if (!bindings.has(ref)) {
|
|
60
|
+
throw new errors.SuperDocCliError(`Unknown plan binding: ${ref}`, {
|
|
61
|
+
code: 'INVALID_ARGUMENT',
|
|
62
|
+
details: { ref, path },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const resolved = readBindingPath(bindings.get(ref), path);
|
|
66
|
+
if (resolved === undefined) {
|
|
67
|
+
throw new errors.SuperDocCliError(`Binding ${ref} does not contain path ${path}.`, {
|
|
68
|
+
code: 'INVALID_ARGUMENT',
|
|
69
|
+
details: { ref, path },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return resolved;
|
|
73
|
+
}
|
|
74
|
+
function resolveBindingTokens(value, bindings) {
|
|
75
|
+
if (isRefToken(value)) {
|
|
76
|
+
return resolveBindingRef(bindings, value.ref, value.path);
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(value)) {
|
|
79
|
+
return value.map((entry) => resolveBindingTokens(entry, bindings));
|
|
80
|
+
}
|
|
81
|
+
if (!isRecord(value))
|
|
82
|
+
return value;
|
|
83
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolveBindingTokens(entry, bindings)]));
|
|
84
|
+
}
|
|
85
|
+
function extractBoundNodeIds(value) {
|
|
86
|
+
if (typeof value === 'string' && value.length > 0)
|
|
87
|
+
return [value];
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
return [...new Set(value.flatMap((entry) => extractBoundNodeIds(entry)))];
|
|
90
|
+
}
|
|
91
|
+
if (!isRecord(value))
|
|
92
|
+
return [];
|
|
93
|
+
const direct = typeof value.nodeId === 'string' && value.nodeId.length > 0
|
|
94
|
+
? [value.nodeId]
|
|
95
|
+
: typeof value.blockId === 'string' && value.blockId.length > 0
|
|
96
|
+
? [value.blockId]
|
|
97
|
+
: typeof value.startNodeId === 'string' && value.startNodeId.length > 0
|
|
98
|
+
? [value.startNodeId]
|
|
99
|
+
: [];
|
|
100
|
+
if (direct.length > 0)
|
|
101
|
+
return direct;
|
|
102
|
+
if (Array.isArray(value.matched)) {
|
|
103
|
+
return [...new Set(value.matched.flatMap((entry) => extractBoundNodeIds(entry)))];
|
|
104
|
+
}
|
|
105
|
+
const recursiveKeys = ['blocks', 'items', 'cells', 'segments'];
|
|
106
|
+
const nested = [];
|
|
107
|
+
for (const key of recursiveKeys) {
|
|
108
|
+
if (Array.isArray(value[key]))
|
|
109
|
+
nested.push(...extractBoundNodeIds(value[key]));
|
|
110
|
+
}
|
|
111
|
+
return [...new Set(nested)];
|
|
112
|
+
}
|
|
113
|
+
function resolveSelectorWithBindings(snapshot, selector, bindings) {
|
|
114
|
+
if (selector.kind === 'ref') {
|
|
115
|
+
return extractBoundNodeIds(resolveBindingRef(bindings, selector.ref));
|
|
116
|
+
}
|
|
117
|
+
if (selector.kind === 'relative') {
|
|
118
|
+
const targetIds = resolveSelectorWithBindings(snapshot, selector.target, bindings);
|
|
119
|
+
const offset = selector.position === 'before' ? -1 : 1;
|
|
120
|
+
const matches = [];
|
|
121
|
+
for (const targetId of targetIds) {
|
|
122
|
+
const index = snapshot.blocks.findIndex((block) => block.nodeId === targetId);
|
|
123
|
+
if (index === -1)
|
|
124
|
+
continue;
|
|
125
|
+
const sibling = snapshot.blocks[index + offset];
|
|
126
|
+
if (sibling != null)
|
|
127
|
+
matches.push(sibling.nodeId);
|
|
128
|
+
}
|
|
129
|
+
return [...new Set(matches)];
|
|
130
|
+
}
|
|
131
|
+
return docSnapshot.resolveSnapshotSelector(snapshot, selector);
|
|
132
|
+
}
|
|
133
|
+
function resolveDocMethod(doc, operationId) {
|
|
134
|
+
const tokens = operationId.split('.').slice(1);
|
|
135
|
+
let cursor = doc;
|
|
136
|
+
for (const token of tokens) {
|
|
137
|
+
if (cursor == null ||
|
|
138
|
+
(typeof cursor !== 'object' && typeof cursor !== 'function') ||
|
|
139
|
+
!(token in cursor)) {
|
|
140
|
+
throw new errors.SuperDocCliError(`No bound method found for operation ${operationId}.`, {
|
|
141
|
+
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
142
|
+
details: { operationId, token },
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
cursor = cursor[token];
|
|
146
|
+
}
|
|
147
|
+
if (typeof cursor !== 'function') {
|
|
148
|
+
throw new errors.SuperDocCliError(`Resolved member for ${operationId} is not callable.`, {
|
|
149
|
+
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
150
|
+
details: { operationId },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return cursor;
|
|
154
|
+
}
|
|
155
|
+
async function agentInspect(doc, args = {}) {
|
|
156
|
+
return docSnapshot.buildDocumentSnapshot(doc, args);
|
|
157
|
+
}
|
|
158
|
+
function checkAgainstSnapshot(snapshot, check) {
|
|
159
|
+
switch (check.kind) {
|
|
160
|
+
case 'block-text-contains': {
|
|
161
|
+
const block = snapshot.blocks.find((b) => b.nodeId === check.nodeId);
|
|
162
|
+
const passed = !!block && block.text.includes(check.text);
|
|
163
|
+
return { check, passed, detail: passed ? undefined : `block ${check.nodeId} does not contain text` };
|
|
164
|
+
}
|
|
165
|
+
case 'block-text-equals': {
|
|
166
|
+
const block = snapshot.blocks.find((b) => b.nodeId === check.nodeId);
|
|
167
|
+
const passed = !!block && block.text === check.text;
|
|
168
|
+
return { check, passed };
|
|
169
|
+
}
|
|
170
|
+
case 'block-count-delta':
|
|
171
|
+
return { check, passed: false, detail: 'block-count-delta requires a baseline snapshot' };
|
|
172
|
+
case 'list-item-count': {
|
|
173
|
+
const list = check.listId == null ? snapshot.lists[0] : snapshot.lists.find((l) => l.listId === check.listId);
|
|
174
|
+
const passed = !!list && list.items.length === check.expected;
|
|
175
|
+
return { check, passed, detail: passed ? undefined : `expected ${check.expected} items` };
|
|
176
|
+
}
|
|
177
|
+
case 'table-shape': {
|
|
178
|
+
const table = snapshot.tables.find((t) => t.nodeId === check.nodeId);
|
|
179
|
+
const passed = !!table && table.rows === check.rows && table.columns === check.columns;
|
|
180
|
+
return { check, passed };
|
|
181
|
+
}
|
|
182
|
+
case 'comment-count-delta':
|
|
183
|
+
return { check, passed: false, detail: 'comment-count-delta requires a baseline snapshot' };
|
|
184
|
+
case 'tracked-change-count-delta':
|
|
185
|
+
return { check, passed: false, detail: 'tracked-change-count-delta requires a baseline snapshot' };
|
|
186
|
+
case 'image-anchor-present': {
|
|
187
|
+
const passed = check.imageId == null
|
|
188
|
+
? snapshot.images.length > 0
|
|
189
|
+
: snapshot.images.some((img) => img.imageId === check.imageId);
|
|
190
|
+
return { check, passed };
|
|
191
|
+
}
|
|
192
|
+
case 'revision-changed':
|
|
193
|
+
return { check, passed: false, detail: 'revision-changed requires a baseline snapshot' };
|
|
194
|
+
case 'revision-unchanged':
|
|
195
|
+
return { check, passed: false, detail: 'revision-unchanged requires a baseline snapshot' };
|
|
196
|
+
case 'document-saves-cleanly':
|
|
197
|
+
return { check, passed: false, detail: 'document-saves-cleanly requires save/reopen evidence' };
|
|
198
|
+
case 'save-reopen-text-contains':
|
|
199
|
+
return { check, passed: false, detail: 'save-reopen-text-contains requires save/reopen evidence' };
|
|
200
|
+
default:
|
|
201
|
+
// Action-evidence check kinds (range-converted, placement-honored, …)
|
|
202
|
+
// are computed inline by the action that emitted them; they cannot be
|
|
203
|
+
// re-evaluated from a bare snapshot. Fail closed.
|
|
204
|
+
return { check, passed: false, detail: 'action-evidence check; not re-checkable from a snapshot' };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function computeDeltaChecks(pre, post, checks, saveReopen) {
|
|
208
|
+
const results = [];
|
|
209
|
+
for (const check of checks) {
|
|
210
|
+
if (check.kind === 'block-count-delta') {
|
|
211
|
+
const preCount = pre.blocks.filter((b) => b.nodeType === check.nodeType).length;
|
|
212
|
+
const postCount = post.blocks.filter((b) => b.nodeType === check.nodeType).length;
|
|
213
|
+
results.push({
|
|
214
|
+
check,
|
|
215
|
+
passed: postCount - preCount === check.delta,
|
|
216
|
+
detail: `pre=${preCount} post=${postCount} delta=${postCount - preCount}`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else if (check.kind === 'comment-count-delta') {
|
|
220
|
+
results.push({
|
|
221
|
+
check,
|
|
222
|
+
passed: post.comments.length - pre.comments.length === check.delta,
|
|
223
|
+
detail: `pre=${pre.comments.length} post=${post.comments.length}`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else if (check.kind === 'tracked-change-count-delta') {
|
|
227
|
+
results.push({
|
|
228
|
+
check,
|
|
229
|
+
passed: post.trackedChanges.length - pre.trackedChanges.length === check.delta,
|
|
230
|
+
detail: `pre=${pre.trackedChanges.length} post=${post.trackedChanges.length}`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
else if (check.kind === 'revision-changed') {
|
|
234
|
+
results.push({
|
|
235
|
+
check,
|
|
236
|
+
passed: pre.revision !== post.revision,
|
|
237
|
+
detail: `pre=${pre.revision} post=${post.revision}`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
else if (check.kind === 'revision-unchanged') {
|
|
241
|
+
results.push({
|
|
242
|
+
check,
|
|
243
|
+
passed: pre.revision === post.revision,
|
|
244
|
+
detail: `pre=${pre.revision} post=${post.revision}`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
else if (check.kind === 'document-saves-cleanly') {
|
|
248
|
+
results.push({
|
|
249
|
+
check,
|
|
250
|
+
passed: saveReopen?.attempted === true && saveReopen.succeeded === true,
|
|
251
|
+
detail: saveReopen?.message,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
else if (check.kind === 'save-reopen-text-contains') {
|
|
255
|
+
const found = saveReopen?.snapshot?.blocks.some((block) => block.text.includes(check.text)) ?? false;
|
|
256
|
+
results.push({
|
|
257
|
+
check,
|
|
258
|
+
passed: saveReopen?.attempted === true && saveReopen.succeeded === true && found,
|
|
259
|
+
detail: found ? undefined : `text not found after save/reopen: ${check.text}`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
results.push(checkAgainstSnapshot(post, check));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return results;
|
|
267
|
+
}
|
|
268
|
+
function computeCurrentChecks(snapshot, checks, saveReopen) {
|
|
269
|
+
return checks.map((check) => {
|
|
270
|
+
if (check.kind === 'document-saves-cleanly') {
|
|
271
|
+
return {
|
|
272
|
+
check,
|
|
273
|
+
passed: saveReopen?.attempted === true && saveReopen.succeeded === true,
|
|
274
|
+
detail: saveReopen?.message,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (check.kind === 'save-reopen-text-contains') {
|
|
278
|
+
const found = saveReopen?.snapshot?.blocks.some((block) => block.text.includes(check.text)) ?? false;
|
|
279
|
+
return {
|
|
280
|
+
check,
|
|
281
|
+
passed: saveReopen?.attempted === true && saveReopen.succeeded === true && found,
|
|
282
|
+
detail: found ? undefined : `text not found after save/reopen: ${check.text}`,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return checkAgainstSnapshot(snapshot, check);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const RISKY_DOMAINS = new Set(['sections', 'protection', 'permission-ranges', 'header-footer', 'content-controls']);
|
|
289
|
+
function planTouchesRiskyDomain(plan) {
|
|
290
|
+
for (const step of plan.steps) {
|
|
291
|
+
if (step.kind !== 'apply')
|
|
292
|
+
continue;
|
|
293
|
+
const entry = operationCatalog.getOperationCatalogEntry(step.operationId);
|
|
294
|
+
if (entry && RISKY_DOMAINS.has(entry.domain))
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
async function trySaveReopen(doc, checks) {
|
|
300
|
+
const saveAny = doc.save;
|
|
301
|
+
if (typeof saveAny !== 'function') {
|
|
302
|
+
return { attempted: false, succeeded: false, message: 'save not available on doc handle' };
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await saveAny.call(doc, {});
|
|
306
|
+
// Rebuild a fresh snapshot after save. Host-level true reopen still needs
|
|
307
|
+
// a new document handle, which this runtime cannot force on its own.
|
|
308
|
+
const fresh = await docSnapshot.buildDocumentSnapshot(doc);
|
|
309
|
+
for (const check of checks) {
|
|
310
|
+
if (check.kind === 'save-reopen-text-contains') {
|
|
311
|
+
const found = fresh.blocks.some((b) => b.text.includes(check.text));
|
|
312
|
+
if (!found) {
|
|
313
|
+
return {
|
|
314
|
+
attempted: true,
|
|
315
|
+
succeeded: false,
|
|
316
|
+
message: `text not found after save: ${check.text}`,
|
|
317
|
+
snapshot: fresh,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { attempted: true, succeeded: true, snapshot: fresh };
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
return { attempted: true, succeeded: false, message: err instanceof Error ? err.message : String(err) };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function verificationNeedsSaveReopen(checks) {
|
|
329
|
+
return checks.some((check) => check.kind === 'document-saves-cleanly' || check.kind === 'save-reopen-text-contains');
|
|
330
|
+
}
|
|
331
|
+
async function agentApply(doc, args) {
|
|
332
|
+
const plan = args.plan;
|
|
333
|
+
const validation = ir.validatePlan(plan);
|
|
334
|
+
if (!validation.ok) {
|
|
335
|
+
return {
|
|
336
|
+
status: 'failed',
|
|
337
|
+
intent: plan.intent,
|
|
338
|
+
preSnapshot: { revision: 'unknown', counts: emptyCounts() },
|
|
339
|
+
selectedTargets: [],
|
|
340
|
+
executedOperations: [],
|
|
341
|
+
verification: [],
|
|
342
|
+
errors: validation.errors.map((e) => ({ code: e.code, message: e.message })),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const preSnapshot = await docSnapshot.buildDocumentSnapshot(doc);
|
|
346
|
+
const selectedTargets = [];
|
|
347
|
+
const executedOperations = [];
|
|
348
|
+
const bindings = new Map();
|
|
349
|
+
try {
|
|
350
|
+
for (const step of plan.steps) {
|
|
351
|
+
if (step.kind === 'select') {
|
|
352
|
+
const matched = resolveSelectorWithBindings(preSnapshot, step.selector, bindings);
|
|
353
|
+
if (step.requireUnique && matched.length !== 1) {
|
|
354
|
+
throw new docSnapshot.AmbiguousSelectorError(`Selector did not resolve uniquely (matched ${matched.length}).`, matched.map((nodeId) => ({ nodeId, description: nodeId })));
|
|
355
|
+
}
|
|
356
|
+
selectedTargets.push({ selector: step.selector, matched });
|
|
357
|
+
if (step.bind) {
|
|
358
|
+
bindings.set(step.bind, matched.length === 1 ? matched[0] : [...matched]);
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (step.kind === 'inspect') {
|
|
363
|
+
const inspectArgs = ensureClean(resolveBindingTokens(step.args, bindings));
|
|
364
|
+
const method = resolveDocMethod(doc, step.operationId);
|
|
365
|
+
const result = await method(inspectArgs);
|
|
366
|
+
if (step.bind)
|
|
367
|
+
bindings.set(step.bind, result);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (step.kind === 'apply') {
|
|
371
|
+
ensureKnownOperation(step.operationId);
|
|
372
|
+
const applyArgs = ensureClean(resolveBindingTokens(step.args, bindings));
|
|
373
|
+
const method = resolveDocMethod(doc, step.operationId);
|
|
374
|
+
const argsWithMode = step.changeMode != null && operationCatalog.getOperationCatalogEntry(step.operationId)?.supportsChangeMode
|
|
375
|
+
? { ...applyArgs, changeMode: step.changeMode }
|
|
376
|
+
: applyArgs;
|
|
377
|
+
const result = await method(argsWithMode);
|
|
378
|
+
executedOperations.push({ operationId: step.operationId, rationale: step.rationale, result });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
if (err instanceof docSnapshot.AmbiguousSelectorError) {
|
|
384
|
+
return {
|
|
385
|
+
status: 'aborted',
|
|
386
|
+
intent: plan.intent,
|
|
387
|
+
preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
|
|
388
|
+
selectedTargets,
|
|
389
|
+
executedOperations,
|
|
390
|
+
verification: [],
|
|
391
|
+
errors: [
|
|
392
|
+
{
|
|
393
|
+
code: err.code,
|
|
394
|
+
message: err.message,
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
400
|
+
return {
|
|
401
|
+
status: 'failed',
|
|
402
|
+
intent: plan.intent,
|
|
403
|
+
preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
|
|
404
|
+
selectedTargets,
|
|
405
|
+
executedOperations,
|
|
406
|
+
verification: [],
|
|
407
|
+
errors: [{ code: 'APPLY_FAILED', message }],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const postSnapshot = await docSnapshot.buildDocumentSnapshot(doc);
|
|
411
|
+
const verifyStep = plan.steps.find((s) => s.kind === 'verify');
|
|
412
|
+
let saveReopen;
|
|
413
|
+
const shouldSaveReopen = (verifyStep?.kind === 'verify' && (verifyStep.saveReopen || verificationNeedsSaveReopen(verifyStep.checks))) ||
|
|
414
|
+
planTouchesRiskyDomain(plan);
|
|
415
|
+
if (shouldSaveReopen) {
|
|
416
|
+
saveReopen = await trySaveReopen(doc, verifyStep?.kind === 'verify' ? verifyStep.checks : []);
|
|
417
|
+
}
|
|
418
|
+
const verification = verifyStep?.kind === 'verify' ? computeDeltaChecks(preSnapshot, postSnapshot, verifyStep.checks, saveReopen) : [];
|
|
419
|
+
const allVerified = verification.every((v) => v.passed);
|
|
420
|
+
return {
|
|
421
|
+
status: allVerified ? 'ok' : 'failed',
|
|
422
|
+
intent: plan.intent,
|
|
423
|
+
preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
|
|
424
|
+
postSnapshot: { revision: postSnapshot.revision, counts: postSnapshot.counts },
|
|
425
|
+
selectedTargets,
|
|
426
|
+
executedOperations,
|
|
427
|
+
verification,
|
|
428
|
+
saveReopen,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
async function agentVerify(doc, args) {
|
|
432
|
+
const snapshot = await docSnapshot.buildDocumentSnapshot(doc);
|
|
433
|
+
let saveReopen;
|
|
434
|
+
if (args.saveReopen || verificationNeedsSaveReopen(args.checks)) {
|
|
435
|
+
saveReopen = await trySaveReopen(doc, args.checks);
|
|
436
|
+
}
|
|
437
|
+
const verification = computeCurrentChecks(snapshot, args.checks, saveReopen);
|
|
438
|
+
const allPassed = verification.every((v) => v.passed);
|
|
439
|
+
return {
|
|
440
|
+
status: allPassed ? 'ok' : 'failed',
|
|
441
|
+
intent: 'verify',
|
|
442
|
+
preSnapshot: { revision: snapshot.revision, counts: snapshot.counts },
|
|
443
|
+
postSnapshot: { revision: snapshot.revision, counts: snapshot.counts },
|
|
444
|
+
selectedTargets: [],
|
|
445
|
+
executedOperations: [],
|
|
446
|
+
verification,
|
|
447
|
+
saveReopen,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Controlled escape hatch — dispatches a single generated operation by id.
|
|
452
|
+
* The catalog determines mode (read/write); if `readOnly` is true and the
|
|
453
|
+
* operation is mutating, the call fails closed.
|
|
454
|
+
*/
|
|
455
|
+
async function agentOperation(doc, args) {
|
|
456
|
+
const entry = ensureKnownOperation(args.operationId);
|
|
457
|
+
if (args.readOnly && entry.isMutating) {
|
|
458
|
+
throw new errors.SuperDocCliError(`Operation ${args.operationId} is mutating and readOnly was requested.`, {
|
|
459
|
+
code: 'INVALID_ARGUMENT',
|
|
460
|
+
details: { operationId: args.operationId },
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const callArgs = ensureClean(args.args ?? {});
|
|
464
|
+
// Sanity-check the operation exists in the generated CONTRACT (and not just
|
|
465
|
+
// our derived catalog). This protects against drift between contract.ts
|
|
466
|
+
// and the doc-api binding.
|
|
467
|
+
if (!contract.CONTRACT.operations[args.operationId]) {
|
|
468
|
+
throw new errors.SuperDocCliError(`Operation ${args.operationId} is not in the generated contract.`, {
|
|
469
|
+
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
470
|
+
details: { operationId: args.operationId },
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
const method = resolveDocMethod(doc, args.operationId);
|
|
474
|
+
return method(callArgs);
|
|
475
|
+
}
|
|
476
|
+
function emptyCounts() {
|
|
477
|
+
return {
|
|
478
|
+
blocks: 0,
|
|
479
|
+
paragraphs: 0,
|
|
480
|
+
headings: 0,
|
|
481
|
+
tables: 0,
|
|
482
|
+
lists: 0,
|
|
483
|
+
images: 0,
|
|
484
|
+
comments: 0,
|
|
485
|
+
trackedChanges: 0,
|
|
486
|
+
sections: 0,
|
|
487
|
+
fields: 0,
|
|
488
|
+
hyperlinks: 0,
|
|
489
|
+
bookmarks: 0,
|
|
490
|
+
contentControls: 0,
|
|
491
|
+
permissionRanges: 0,
|
|
492
|
+
styles: 0,
|
|
493
|
+
headers: 0,
|
|
494
|
+
footers: 0,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
exports.agentApply = agentApply;
|
|
499
|
+
exports.agentInspect = agentInspect;
|
|
500
|
+
exports.agentOperation = agentOperation;
|
|
501
|
+
exports.agentVerify = agentVerify;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clean agent runtime.
|
|
3
|
+
*
|
|
4
|
+
* Exposes the four product-stable agent tools:
|
|
5
|
+
*
|
|
6
|
+
* - `superdoc_inspect` — build a deterministic document snapshot
|
|
7
|
+
* - `agent_apply` — execute a validated IR plan against a document
|
|
8
|
+
* - `agent_verify` — re-run verification checks against current state
|
|
9
|
+
* - `agent_operation` — controlled escape hatch that dispatches a single
|
|
10
|
+
* generated `doc.*` operation by id
|
|
11
|
+
*
|
|
12
|
+
* Every mutating call returns receipts containing pre/post evidence,
|
|
13
|
+
* selected targets, executed operations, verification checks, and (for risky
|
|
14
|
+
* domains) save/reopen proof.
|
|
15
|
+
*
|
|
16
|
+
* This runtime does not depend on, or import, the benchmark-shaped
|
|
17
|
+
* `workflow-poc` profile. The benchmark surface is preserved separately for
|
|
18
|
+
* measurement, but agent_* product paths never route through it.
|
|
19
|
+
*/
|
|
20
|
+
import type { BoundDocApi } from '../generated/client.js';
|
|
21
|
+
import { type AgentPlan, type AgentVerificationCheck } from './ir.js';
|
|
22
|
+
import { type DocumentSnapshot, type SnapshotDomain } from './doc-snapshot.js';
|
|
23
|
+
export type AgentInspectArgs = {
|
|
24
|
+
countsOnly?: boolean;
|
|
25
|
+
includeDomains?: readonly SnapshotDomain[];
|
|
26
|
+
blockNodeTypes?: readonly string[];
|
|
27
|
+
blockTextLimit?: number;
|
|
28
|
+
listLimit?: number;
|
|
29
|
+
tableLimit?: number;
|
|
30
|
+
commentLimit?: number;
|
|
31
|
+
trackedChangeLimit?: number;
|
|
32
|
+
blockOffset?: number;
|
|
33
|
+
blockLimit?: number;
|
|
34
|
+
omitEmptyBlocks?: boolean;
|
|
35
|
+
dropTextPreview?: boolean;
|
|
36
|
+
};
|
|
37
|
+
export type AgentApplyArgs = {
|
|
38
|
+
plan: AgentPlan;
|
|
39
|
+
};
|
|
40
|
+
export type AgentVerifyArgs = {
|
|
41
|
+
checks: readonly AgentVerificationCheck[];
|
|
42
|
+
saveReopen?: boolean;
|
|
43
|
+
};
|
|
44
|
+
export type AgentOperationArgs = {
|
|
45
|
+
operationId: string;
|
|
46
|
+
args?: Record<string, unknown>;
|
|
47
|
+
/** When true, fail closed for any operation classified as mutating. Default false. */
|
|
48
|
+
readOnly?: boolean;
|
|
49
|
+
};
|
|
50
|
+
export type VerificationResult = {
|
|
51
|
+
check: AgentVerificationCheck;
|
|
52
|
+
passed: boolean;
|
|
53
|
+
detail?: string;
|
|
54
|
+
};
|
|
55
|
+
export type SelectedTarget = {
|
|
56
|
+
selector: import('./ir.js').AgentSelector;
|
|
57
|
+
matched: readonly string[];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Machine-readable recovery guidance. The prose twins (`revertHint`,
|
|
61
|
+
* teaching-error messages) are what the model reads; this is the same
|
|
62
|
+
* instruction as data, for customer logging and automated retry tooling.
|
|
63
|
+
*/
|
|
64
|
+
export type ReceiptRecovery = {
|
|
65
|
+
kind: 'retry' | 'revert' | 'reinspect';
|
|
66
|
+
/** Literal next call, paste-ready, e.g. `superdoc_perform_action {"action":"undo_changes","untilMarker":"2.1."}` */
|
|
67
|
+
call?: string;
|
|
68
|
+
selector?: Record<string, unknown>;
|
|
69
|
+
};
|
|
70
|
+
export type AgentReceipt = {
|
|
71
|
+
/**
|
|
72
|
+
* `partial` means SOME requested edits applied and some did not — receipts
|
|
73
|
+
* must never report a half-done request as plain ok.
|
|
74
|
+
*/
|
|
75
|
+
status: 'ok' | 'partial' | 'failed' | 'aborted';
|
|
76
|
+
intent: string;
|
|
77
|
+
preSnapshot?: {
|
|
78
|
+
revision: string;
|
|
79
|
+
counts?: DocumentSnapshot['counts'];
|
|
80
|
+
};
|
|
81
|
+
postSnapshot?: {
|
|
82
|
+
revision: string;
|
|
83
|
+
counts?: DocumentSnapshot['counts'];
|
|
84
|
+
};
|
|
85
|
+
selectedTargets?: readonly SelectedTarget[];
|
|
86
|
+
executedOperations?: ReadonlyArray<{
|
|
87
|
+
operationId: string;
|
|
88
|
+
rationale?: string;
|
|
89
|
+
result?: unknown;
|
|
90
|
+
}>;
|
|
91
|
+
verification?: ReadonlyArray<VerificationResult>;
|
|
92
|
+
saveReopen?: {
|
|
93
|
+
attempted: boolean;
|
|
94
|
+
succeeded: boolean;
|
|
95
|
+
message?: string;
|
|
96
|
+
};
|
|
97
|
+
errors?: ReadonlyArray<{
|
|
98
|
+
code: string;
|
|
99
|
+
message: string;
|
|
100
|
+
recovery?: ReceiptRecovery;
|
|
101
|
+
}>;
|
|
102
|
+
/** What the model should do next when status is partial/failed. Prose. */
|
|
103
|
+
nextStep?: string;
|
|
104
|
+
/** Literal revert call when the action dispatched multiple history steps. */
|
|
105
|
+
revertHint?: string;
|
|
106
|
+
recovery?: ReceiptRecovery;
|
|
107
|
+
note?: string;
|
|
108
|
+
/** Action-specific evidence (editsApplied, marker, placement, …). */
|
|
109
|
+
[key: string]: unknown;
|
|
110
|
+
};
|
|
111
|
+
export declare function agentInspect(doc: BoundDocApi, args?: AgentInspectArgs): Promise<DocumentSnapshot>;
|
|
112
|
+
export declare function agentApply(doc: BoundDocApi, args: AgentApplyArgs): Promise<AgentReceipt>;
|
|
113
|
+
export declare function agentVerify(doc: BoundDocApi, args: AgentVerifyArgs): Promise<AgentReceipt>;
|
|
114
|
+
/**
|
|
115
|
+
* Controlled escape hatch — dispatches a single generated operation by id.
|
|
116
|
+
* The catalog determines mode (read/write); if `readOnly` is true and the
|
|
117
|
+
* operation is mutating, the call fails closed.
|
|
118
|
+
*/
|
|
119
|
+
export declare function agentOperation(doc: BoundDocApi, args: AgentOperationArgs): Promise<unknown>;
|
|
120
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/agent/runtime.ts"],"names":[],"mappings":"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"}
|