@superdoc/sdk 2.11.0-next.4 → 2.11.0-next.5
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/actions/define.cjs +788 -0
- package/dist/actions/define.d.ts +142 -0
- package/dist/actions/define.js +781 -0
- package/dist/agent/catalog.cjs +1 -0
- package/dist/agent/catalog.d.ts +8 -0
- package/dist/agent/catalog.js +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/presets.cjs +2 -2
- package/dist/presets.js +2 -2
- 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 +49 -1
- package/dist/tools.d.ts +18 -1
- package/dist/tools.js +49 -1
- package/package.json +9 -9
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var errors = require('../runtime/errors.cjs');
|
|
4
|
+
var presets = require('../presets.cjs');
|
|
5
|
+
var actions = require('../agent/actions.cjs');
|
|
6
|
+
var catalog = require('../agent/catalog.cjs');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Customer-extensible **custom actions** for the SuperDoc LLM-tools SDK. The
|
|
10
|
+
* canonical ActionSpec has exactly ONE execution tier:
|
|
11
|
+
*
|
|
12
|
+
* - `steps` — declarative composition of built-in core actions with
|
|
13
|
+
* {{arg}} templating; dispatches through the base preset and
|
|
14
|
+
* inherits its target resolution, receipts, and verification.
|
|
15
|
+
* - `run` — a native function executed in the CALLER'S process against
|
|
16
|
+
* the typed session-bound doc handle, with synthesized
|
|
17
|
+
* truth-telling receipts (pre/post revision, partialMutation).
|
|
18
|
+
*
|
|
19
|
+
* NOTE: a third, IN-HOST tier — a JS function-expression run inside the
|
|
20
|
+
* document host via `superdoc_execute_code` — is intentionally not part of the
|
|
21
|
+
* kit yet. It lands together with the code-act execution path (and its safety
|
|
22
|
+
* envelope); until then the kit exposes only `steps` and `run`.
|
|
23
|
+
*
|
|
24
|
+
* `extendPreset` merges custom actions into the
|
|
25
|
+
* superdoc_perform_action enum, tool description, system prompt, and dispatch
|
|
26
|
+
* COHERENTLY — including excludeActions, which may name built-in actions
|
|
27
|
+
* (forwarded to the base) or custom ones (handled by the wrapper). No CLI
|
|
28
|
+
* host changes are required by either tier.
|
|
29
|
+
*
|
|
30
|
+
* Cross-runtime contract: templating semantics, input-schema defaults, and
|
|
31
|
+
* receipt shapes are identical to the Python mirror
|
|
32
|
+
* (`langs/python/superdoc/presets/custom.py`) for any JSON-serializable tool
|
|
33
|
+
* input. (NaN/Infinity/lone-surrogates are out of scope: they cannot appear in
|
|
34
|
+
* a JSON tool call.)
|
|
35
|
+
*
|
|
36
|
+
* @module
|
|
37
|
+
*/
|
|
38
|
+
/** Which execution tier a spec uses. */
|
|
39
|
+
function executionKindOf(spec) {
|
|
40
|
+
return Array.isArray(spec.steps) ? 'steps' : 'run';
|
|
41
|
+
}
|
|
42
|
+
const BUILTIN_ACTION_NAMES = new Set(actions.ACTION_NAMES_LIST);
|
|
43
|
+
function isJsonSchemaObject(value) {
|
|
44
|
+
return (value != null &&
|
|
45
|
+
typeof value === 'object' &&
|
|
46
|
+
!Array.isArray(value) &&
|
|
47
|
+
value.type === 'object');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Normalize the `input` field to a JSON Schema object. We take NO schema-library
|
|
51
|
+
* dependency: a Zod (or Zod-like) schema is rejected with a clear, actionable
|
|
52
|
+
* error instead of being silently accepted with its types dropped — the caller
|
|
53
|
+
* converts it first (e.g. `zod-to-json-schema`). A plain JSON Schema object is
|
|
54
|
+
* the primary path; a bare properties bag is wrapped for convenience.
|
|
55
|
+
*/
|
|
56
|
+
function coerceInputSchema(input) {
|
|
57
|
+
if (input == null) {
|
|
58
|
+
return { type: 'object', properties: {}, additionalProperties: true };
|
|
59
|
+
}
|
|
60
|
+
// Zod / Zod-like schemas expose `_def` (and usually `.parse`/`.safeParse`).
|
|
61
|
+
// Reject clearly rather than half-supporting them (dropping constraints).
|
|
62
|
+
// Checked BEFORE isJsonSchemaObject so a library schema can never slip through.
|
|
63
|
+
const duck = input;
|
|
64
|
+
if (duck._def != null || typeof duck.parse === 'function' || typeof duck.safeParse === 'function') {
|
|
65
|
+
throw new errors.SuperDocCliError('defineAction `input` must be a JSON Schema object, not a Zod (or other library) schema. ' +
|
|
66
|
+
'Convert it first, e.g. `import { zodToJsonSchema } from "zod-to-json-schema"; ' +
|
|
67
|
+
'defineAction({ input: zodToJsonSchema(mySchema), ... })`.', { code: 'INVALID_ARGUMENT' });
|
|
68
|
+
}
|
|
69
|
+
if (isJsonSchemaObject(input)) {
|
|
70
|
+
// A valid object schema may legally omit `properties` (an open schema like
|
|
71
|
+
// `{ type: 'object', additionalProperties: true }`). The declared type makes
|
|
72
|
+
// `properties` required and every consumer (and the Python mirror) assumes
|
|
73
|
+
// it exists, so normalize a missing/non-object bag to {}.
|
|
74
|
+
return isRecord(input.properties) ? input : { ...input, properties: {} };
|
|
75
|
+
}
|
|
76
|
+
// A bare properties bag — wrap it.
|
|
77
|
+
return { type: 'object', properties: input, additionalProperties: true };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Author a {@link ActionSpec}. Pass exactly one execution tier: `steps`
|
|
81
|
+
* (declarative — recommended) or `run` (native function in your process).
|
|
82
|
+
*/
|
|
83
|
+
function defineAction(input) {
|
|
84
|
+
if (typeof input.name !== 'string' || input.name.length === 0) {
|
|
85
|
+
throw new errors.SuperDocCliError('defineAction requires a non-empty `name`.', {
|
|
86
|
+
code: 'INVALID_ARGUMENT',
|
|
87
|
+
details: { input: 'name' },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (typeof input.description !== 'string') {
|
|
91
|
+
throw new errors.SuperDocCliError(`defineAction "${input.name}" requires a string \`description\`.`, {
|
|
92
|
+
code: 'INVALID_ARGUMENT',
|
|
93
|
+
details: { name: input.name },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const tiers = [];
|
|
97
|
+
if (Array.isArray(input.steps))
|
|
98
|
+
tiers.push('steps');
|
|
99
|
+
if (typeof input.run === 'function')
|
|
100
|
+
tiers.push('run');
|
|
101
|
+
if (tiers.length !== 1) {
|
|
102
|
+
throw new errors.SuperDocCliError(`defineAction "${input.name}" requires exactly one of \`steps\` or \`run\` (got ${tiers.length === 0 ? 'none' : tiers.join(' + ')}).`, { code: 'INVALID_ARGUMENT', details: { name: input.name, tiers } });
|
|
103
|
+
}
|
|
104
|
+
const spec = {
|
|
105
|
+
name: input.name,
|
|
106
|
+
description: input.description,
|
|
107
|
+
inputSchema: coerceInputSchema(input.input),
|
|
108
|
+
};
|
|
109
|
+
if (tiers[0] === 'steps') {
|
|
110
|
+
const steps = input.steps;
|
|
111
|
+
if (steps.length === 0) {
|
|
112
|
+
throw new errors.SuperDocCliError(`defineAction "${input.name}": \`steps\` must be a non-empty array.`, {
|
|
113
|
+
code: 'INVALID_ARGUMENT',
|
|
114
|
+
details: { name: input.name },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
steps.forEach((step, index) => {
|
|
118
|
+
if (step == null || typeof step.action !== 'string' || step.action.length === 0) {
|
|
119
|
+
throw new errors.SuperDocCliError(`defineAction "${input.name}": steps[${index}] needs a non-empty \`action\` string.`, { code: 'INVALID_ARGUMENT', details: { name: input.name, index } });
|
|
120
|
+
}
|
|
121
|
+
if (!BUILTIN_ACTION_NAMES.has(step.action)) {
|
|
122
|
+
throw new errors.SuperDocCliError(`defineAction "${input.name}": steps[${index}].action "${step.action}" is not a built-in core action. Steps compose built-in actions only; use the \`run\` tier for anything else.`, { code: 'INVALID_ARGUMENT', details: { name: input.name, index, action: step.action } });
|
|
123
|
+
}
|
|
124
|
+
if (step.args != null && !isRecord(step.args)) {
|
|
125
|
+
throw new errors.SuperDocCliError(`defineAction "${input.name}": steps[${index}].args must be an object.`, {
|
|
126
|
+
code: 'INVALID_ARGUMENT',
|
|
127
|
+
details: { name: input.name, index },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
spec.steps = steps.map((step) => ({ action: step.action, args: { ...step.args } }));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
spec.run = input.run;
|
|
135
|
+
}
|
|
136
|
+
return spec;
|
|
137
|
+
}
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Codegen — identical to the Python mirror for JSON-serializable tool inputs
|
|
140
|
+
// (the only values a tool call can carry; NaN/Infinity/lone-surrogates are out
|
|
141
|
+
// of scope and serialize differently across Node and Python).
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Collision / duplicate validation
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
/**
|
|
146
|
+
* Provider tool-name rule. OpenAI/Anthropic require tool names to match
|
|
147
|
+
* `^[A-Za-z0-9_-]{1,64}$` — dots are invalid. This only matters in STANDALONE
|
|
148
|
+
* mode, where the action name becomes a tool name; in MERGED mode the name is
|
|
149
|
+
* an enum VALUE (dots are fine).
|
|
150
|
+
*/
|
|
151
|
+
const PROVIDER_SAFE_TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
152
|
+
/** Tool names of the agent surface itself — a custom action must never shadow one. */
|
|
153
|
+
const RESERVED_TOOL_NAMES = new Set(catalog.AGENT_TOOL_NAMES);
|
|
154
|
+
/**
|
|
155
|
+
* excludeActions may name BUILT-IN
|
|
156
|
+
* actions (forwarded to the base, which validates them) or CUSTOM actions
|
|
157
|
+
* (handled by the wrapper — the base would reject names it doesn't know).
|
|
158
|
+
* Both halves narrow tools, prompt, and dispatch together, preserving the
|
|
159
|
+
* kit's coherence guarantee.
|
|
160
|
+
*/
|
|
161
|
+
function splitCustomExclusions(byName, list) {
|
|
162
|
+
if (!list || list.length === 0)
|
|
163
|
+
return { customExcluded: new Set(), builtinExcluded: undefined };
|
|
164
|
+
const customExcluded = new Set();
|
|
165
|
+
const builtin = [];
|
|
166
|
+
for (const name of list) {
|
|
167
|
+
if (byName.has(name))
|
|
168
|
+
customExcluded.add(name);
|
|
169
|
+
else
|
|
170
|
+
builtin.push(name);
|
|
171
|
+
}
|
|
172
|
+
return { customExcluded, builtinExcluded: builtin.length > 0 ? builtin : undefined };
|
|
173
|
+
}
|
|
174
|
+
/** Defense-in-depth refusal shared by both wrappers' dispatchers. */
|
|
175
|
+
function throwExcludedAction(toolName, actionName, extra = {}) {
|
|
176
|
+
throw new errors.SuperDocCliError(`Action ${actionName} is excluded by configuration.`, {
|
|
177
|
+
code: 'INVALID_ARGUMENT',
|
|
178
|
+
details: { toolName, action: actionName, excluded: true, ...extra },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Drop surface `excludeActions` from invoke options before INTERNAL step
|
|
183
|
+
* dispatch. Those exclusions govern what the MODEL may call directly (the
|
|
184
|
+
* top-level dispatch already enforced them); a steps-tier custom action is an
|
|
185
|
+
* authored composition whose steps must run even when a built-in they compose
|
|
186
|
+
* is hidden from the model. Everything else in invokeOptions passes through.
|
|
187
|
+
*/
|
|
188
|
+
function stripSurfaceExclusions(invokeOptions) {
|
|
189
|
+
if (!invokeOptions || !('excludeActions' in invokeOptions))
|
|
190
|
+
return invokeOptions;
|
|
191
|
+
const { excludeActions: _dropped, ...rest } = invokeOptions;
|
|
192
|
+
return rest;
|
|
193
|
+
}
|
|
194
|
+
function assertActionsValid(actions, presetId, standalone = false) {
|
|
195
|
+
const seen = new Set();
|
|
196
|
+
for (const action of actions) {
|
|
197
|
+
// Raw spec objects can bypass defineAction — re-validate the tier shape
|
|
198
|
+
// here so a hand-rolled {steps: []} can't fabricate succeeded receipts.
|
|
199
|
+
const tiers = [
|
|
200
|
+
Array.isArray(action.steps) ? 'steps' : null,
|
|
201
|
+
typeof action.run === 'function' ? 'run' : null,
|
|
202
|
+
].filter((tier) => tier != null);
|
|
203
|
+
if (tiers.length !== 1) {
|
|
204
|
+
throw new errors.SuperDocCliError(`Custom action "${action.name}" must have exactly one of steps/run (got ${tiers.length === 0 ? 'none' : tiers.join(' + ')}).`, { code: 'INVALID_ARGUMENT', details: { presetId, name: action.name, tiers } });
|
|
205
|
+
}
|
|
206
|
+
if (Array.isArray(action.steps)) {
|
|
207
|
+
if (action.steps.length === 0) {
|
|
208
|
+
throw new errors.SuperDocCliError(`Custom action "${action.name}": steps must be non-empty.`, {
|
|
209
|
+
code: 'INVALID_ARGUMENT',
|
|
210
|
+
details: { presetId, name: action.name },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
for (const [index, step] of action.steps.entries()) {
|
|
214
|
+
if (!step || typeof step.action !== 'string' || !BUILTIN_ACTION_NAMES.has(step.action)) {
|
|
215
|
+
throw new errors.SuperDocCliError(`Custom action "${action.name}": steps[${index}].action must be a built-in core action.`, { code: 'INVALID_ARGUMENT', details: { presetId, name: action.name, index } });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (BUILTIN_ACTION_NAMES.has(action.name)) {
|
|
220
|
+
throw new errors.SuperDocCliError(`Custom action "${action.name}" collides with a built-in core action name. Use a namespaced name like "superdoc.${action.name}".`, { code: 'INVALID_ARGUMENT', details: { presetId, name: action.name } });
|
|
221
|
+
}
|
|
222
|
+
if (RESERVED_TOOL_NAMES.has(action.name)) {
|
|
223
|
+
throw new errors.SuperDocCliError(`Custom action "${action.name}" collides with a reserved tool name — it would shadow the agent surface itself.`, { code: 'INVALID_ARGUMENT', details: { presetId, name: action.name } });
|
|
224
|
+
}
|
|
225
|
+
if (seen.has(action.name)) {
|
|
226
|
+
throw new errors.SuperDocCliError(`Duplicate custom action name "${action.name}" in preset "${presetId}".`, {
|
|
227
|
+
code: 'INVALID_ARGUMENT',
|
|
228
|
+
details: { presetId, name: action.name },
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
// In standalone mode the action name becomes a provider tool name, which
|
|
232
|
+
// OpenAI/Anthropic reject unless it matches ^[A-Za-z0-9_-]{1,64}$ (dotted
|
|
233
|
+
// namespaced names are only valid as merged enum VALUES).
|
|
234
|
+
if (standalone && !PROVIDER_SAFE_TOOL_NAME.test(action.name)) {
|
|
235
|
+
throw new errors.SuperDocCliError(`standalone action names must match ^[A-Za-z0-9_-]{1,64}$; "${action.name}" has invalid characters — use merged mode for dotted names.`, { code: 'INVALID_ARGUMENT', details: { presetId, name: action.name } });
|
|
236
|
+
}
|
|
237
|
+
seen.add(action.name);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// runCustomAction — validate, codegen, dispatch via superdoc_execute_code, map receipt
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
function isRecord(value) {
|
|
244
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
245
|
+
}
|
|
246
|
+
/** Kit-level args every custom action accepts without declaring them. */
|
|
247
|
+
const IMPLICIT_ACTION_ARGS = new Set(['changeMode', 'rationale']);
|
|
248
|
+
function validateAgainstSchema(action, args) {
|
|
249
|
+
const required = Array.isArray(action.inputSchema.required) ? action.inputSchema.required : [];
|
|
250
|
+
const missing = required.filter((key) => args[key] == null);
|
|
251
|
+
if (missing.length > 0) {
|
|
252
|
+
throw new errors.SuperDocCliError(`Missing required argument(s) for ${action.name}: ${missing.join(', ')}`, {
|
|
253
|
+
code: 'INVALID_ARGUMENT',
|
|
254
|
+
details: { action: action.name, missingKeys: missing },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const properties = isRecord(action.inputSchema.properties) ? action.inputSchema.properties : {};
|
|
258
|
+
if (action.inputSchema.additionalProperties === false) {
|
|
259
|
+
const unknown = Object.keys(args).filter((key) => !(key in properties) && !IMPLICIT_ACTION_ARGS.has(key));
|
|
260
|
+
if (unknown.length > 0) {
|
|
261
|
+
throw new errors.SuperDocCliError(`Unknown argument(s) for ${action.name}: ${unknown.join(', ')}`, {
|
|
262
|
+
code: 'INVALID_ARGUMENT',
|
|
263
|
+
details: { action: action.name, unknownKeys: unknown, knownKeys: Object.keys(properties) },
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
268
|
+
const value = args[key];
|
|
269
|
+
if (value !== undefined && isRecord(prop) && Array.isArray(prop.enum) && !prop.enum.includes(value)) {
|
|
270
|
+
throw new errors.SuperDocCliError(`Invalid value for ${action.name}.${key}: ${JSON.stringify(value)} (allowed: ${prop.enum.map((entry) => JSON.stringify(entry)).join(', ')})`, { code: 'INVALID_ARGUMENT', details: { action: action.name, key, allowed: prop.enum } });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** Fill in `inputSchema.properties.*.default` values for absent args. */
|
|
275
|
+
function applyInputDefaults(action, args) {
|
|
276
|
+
const out = { ...args };
|
|
277
|
+
for (const [key, prop] of Object.entries(action.inputSchema.properties ?? {})) {
|
|
278
|
+
if (out[key] === undefined && isRecord(prop) && 'default' in prop)
|
|
279
|
+
out[key] = prop.default;
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
const WHOLE_TEMPLATE = /^\{\{(\w+)\}\}$/;
|
|
284
|
+
/**
|
|
285
|
+
* Substitute `{{arg}}` templates in a step's args. A whole-string `"{{x}}"`
|
|
286
|
+
* yields the RAW value (arrays/objects/numbers survive); partial templates
|
|
287
|
+
* interpolate as text. Keys whose whole-string template resolves to undefined
|
|
288
|
+
* are dropped, so optional args don't inject `undefined` into step args.
|
|
289
|
+
*/
|
|
290
|
+
function substituteTemplates(node, vars) {
|
|
291
|
+
if (typeof node === 'string') {
|
|
292
|
+
const whole = WHOLE_TEMPLATE.exec(node);
|
|
293
|
+
if (whole)
|
|
294
|
+
return vars[whole[1]];
|
|
295
|
+
return node.replace(/\{\{(\w+)\}\}/g, (_, name) => {
|
|
296
|
+
const value = vars[name];
|
|
297
|
+
// Text interpolation must be byte-identical across Node and Python:
|
|
298
|
+
// strings verbatim, null/undefined → '', everything else compact JSON
|
|
299
|
+
// (true/[1,2]/{"a":1} — NOT String(), whose array/object/boolean forms
|
|
300
|
+
// differ from Python's str()).
|
|
301
|
+
if (value == null)
|
|
302
|
+
return '';
|
|
303
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (Array.isArray(node)) {
|
|
307
|
+
// Whole-string templates for ABSENT args are dropped from arrays too —
|
|
308
|
+
// leaving undefined behind would serialize as null (and Python's sentinel
|
|
309
|
+
// would crash the transport).
|
|
310
|
+
return node.map((item) => substituteTemplates(item, vars)).filter((item) => item !== undefined);
|
|
311
|
+
}
|
|
312
|
+
if (isRecord(node)) {
|
|
313
|
+
const out = {};
|
|
314
|
+
for (const [key, value] of Object.entries(node)) {
|
|
315
|
+
const substituted = substituteTemplates(value, vars);
|
|
316
|
+
if (substituted !== undefined)
|
|
317
|
+
out[key] = substituted;
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
return node;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* `steps` tier — dispatch each built-in step through the base preset and
|
|
325
|
+
* aggregate per-step receipts. Stops at the first failed step; the aggregate
|
|
326
|
+
* status is `partial` when earlier steps landed, `failed` otherwise.
|
|
327
|
+
*/
|
|
328
|
+
async function runStepsAction(base, action, documentHandle, args, invokeOptions) {
|
|
329
|
+
const vars = args; // defaults already applied by the router
|
|
330
|
+
const rows = [];
|
|
331
|
+
// Steps are the author's curated composition, not model calls — surface
|
|
332
|
+
// exclusions must not refuse a built-in the action deliberately composes.
|
|
333
|
+
const stepOptions = stripSurfaceExclusions(invokeOptions);
|
|
334
|
+
for (const [index, step] of (action.steps ?? []).entries()) {
|
|
335
|
+
let stepArgs = substituteTemplates(step.args ?? {}, vars);
|
|
336
|
+
// changeMode pass-through: a caller-level changeMode reaches every step
|
|
337
|
+
// that doesn't pin its own.
|
|
338
|
+
if (typeof vars.changeMode === 'string' && stepArgs.changeMode === undefined) {
|
|
339
|
+
stepArgs = { ...stepArgs, changeMode: vars.changeMode };
|
|
340
|
+
}
|
|
341
|
+
let receipt;
|
|
342
|
+
try {
|
|
343
|
+
const dispatched = await base.dispatch(documentHandle, 'superdoc_perform_action', { action: step.action, ...stepArgs }, stepOptions);
|
|
344
|
+
receipt = isRecord(dispatched) ? dispatched : { status: 'ok', result: dispatched };
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
// Validation errors THROW from dispatch; runtime failures come back as
|
|
348
|
+
// failed receipts. Normalize both into the per-step row.
|
|
349
|
+
const err = error;
|
|
350
|
+
receipt = { status: 'failed', errors: [{ code: err.code ?? null, message: err.message ?? String(error) }] };
|
|
351
|
+
}
|
|
352
|
+
rows.push({
|
|
353
|
+
step: index,
|
|
354
|
+
action: step.action,
|
|
355
|
+
status: receipt.status,
|
|
356
|
+
...(receipt.verificationPassed !== undefined ? { verificationPassed: receipt.verificationPassed } : {}),
|
|
357
|
+
});
|
|
358
|
+
if (receipt.status === 'failed') {
|
|
359
|
+
const anyLanded = rows.some((row) => row.status !== 'failed');
|
|
360
|
+
return {
|
|
361
|
+
status: anyLanded ? 'partial' : 'failed',
|
|
362
|
+
action: action.name,
|
|
363
|
+
steps: rows,
|
|
364
|
+
failedStep: { index, receipt },
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
// Truthfulness: a step that only PARTIALLY landed (or whose verification
|
|
368
|
+
// disagreed) must not roll up into a clean `succeeded`. Stop and report
|
|
369
|
+
// partial with the evidence — later steps may depend on the missing part.
|
|
370
|
+
if (receipt.status === 'partial' || receipt.verificationPassed === false) {
|
|
371
|
+
return {
|
|
372
|
+
status: 'partial',
|
|
373
|
+
action: action.name,
|
|
374
|
+
steps: rows,
|
|
375
|
+
failedStep: { index, receipt },
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { status: 'succeeded', action: action.name, steps: rows };
|
|
380
|
+
}
|
|
381
|
+
/** Read the session revision off a client-side doc handle, if it can. */
|
|
382
|
+
async function readRevision(documentHandle) {
|
|
383
|
+
const info = documentHandle.info;
|
|
384
|
+
if (typeof info !== 'function')
|
|
385
|
+
return null;
|
|
386
|
+
try {
|
|
387
|
+
const result = (await info.call(documentHandle, {}));
|
|
388
|
+
return result?.revision == null ? null : String(result.revision);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* `run` tier — execute the native function in the caller's process against the
|
|
396
|
+
* typed doc handle, synthesizing a truth-telling receipt: pre/post revision,
|
|
397
|
+
* and on failure whether a partial mutation was left behind + a recovery hint.
|
|
398
|
+
*/
|
|
399
|
+
async function runNativeAction(action, documentHandle, args) {
|
|
400
|
+
const vars = args; // defaults already applied by the router
|
|
401
|
+
const preRevision = await readRevision(documentHandle);
|
|
402
|
+
try {
|
|
403
|
+
const result = await action.run(documentHandle, vars);
|
|
404
|
+
const postRevision = await readRevision(documentHandle);
|
|
405
|
+
return { status: 'succeeded', action: action.name, result, preRevision, postRevision };
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
const err = error;
|
|
409
|
+
const postRevision = await readRevision(documentHandle);
|
|
410
|
+
const partialMutation = preRevision != null && postRevision != null && preRevision !== postRevision;
|
|
411
|
+
return {
|
|
412
|
+
status: 'failed',
|
|
413
|
+
action: action.name,
|
|
414
|
+
errors: [{ code: err.code ?? null, message: err.message ?? String(error) }],
|
|
415
|
+
preRevision,
|
|
416
|
+
postRevision,
|
|
417
|
+
partialMutation,
|
|
418
|
+
recovery: partialMutation
|
|
419
|
+
? { kind: 'revert', call: 'superdoc_perform_action {action:"undo_changes"}' }
|
|
420
|
+
: { kind: 'retry' },
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/** Validate args, then route the custom action to its execution tier. */
|
|
425
|
+
async function runCustomAction(base, action, documentHandle, rawArgs, invokeOptions, fromPerformAction = true) {
|
|
426
|
+
// `action` is the superdoc_perform_action discriminator ONLY on that route;
|
|
427
|
+
// in standalone mode the action is its own tool, so `action` may be a real
|
|
428
|
+
// declared argument — strip it only when it came in as the discriminator.
|
|
429
|
+
const rawRest = { ...rawArgs };
|
|
430
|
+
if (fromPerformAction)
|
|
431
|
+
delete rawRest.action;
|
|
432
|
+
// Apply schema defaults, THEN validate — a required arg with a declared
|
|
433
|
+
// default is satisfiable by the default.
|
|
434
|
+
const args = applyInputDefaults(action, rawRest);
|
|
435
|
+
validateAgainstSchema(action, args);
|
|
436
|
+
return executionKindOf(action) === 'steps'
|
|
437
|
+
? runStepsAction(base, action, documentHandle, args, invokeOptions)
|
|
438
|
+
: runNativeAction(action, documentHandle, args);
|
|
439
|
+
}
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Tool-list merging — mirror the provider shapes from agent/catalog.ts
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
function toolNameOf(tool) {
|
|
444
|
+
const t = tool;
|
|
445
|
+
return t?.function?.name ?? t?.name ?? '';
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Re-apply the Anthropic prompt-cache marker after the tool list was mutated.
|
|
449
|
+
*
|
|
450
|
+
* The base preset places `cache_control: { type: 'ephemeral' }` on its LAST
|
|
451
|
+
* tool. Appending (standalone) or narrowing tools (includeActions) can leave the
|
|
452
|
+
* marker mid-list or drop it entirely. When the marker is meaningful — provider
|
|
453
|
+
* is anthropic and cache was requested — strip any existing `cache_control` and
|
|
454
|
+
* put it back on the final last tool so the cached prefix stays correct. No-op
|
|
455
|
+
* for other providers / when cache was not requested / on an empty list.
|
|
456
|
+
*/
|
|
457
|
+
function renormalizeAnthropicCacheMarker(tools, provider, cacheRequested) {
|
|
458
|
+
if (provider !== 'anthropic' || !cacheRequested || tools.length === 0)
|
|
459
|
+
return tools;
|
|
460
|
+
const stripped = tools.map((tool) => {
|
|
461
|
+
if (!isRecord(tool) || !('cache_control' in tool))
|
|
462
|
+
return tool;
|
|
463
|
+
const { cache_control: _drop, ...rest } = tool;
|
|
464
|
+
return rest;
|
|
465
|
+
});
|
|
466
|
+
const last = stripped[stripped.length - 1];
|
|
467
|
+
stripped[stripped.length - 1] = isRecord(last) ? { ...last, cache_control: { type: 'ephemeral' } } : last;
|
|
468
|
+
return stripped;
|
|
469
|
+
}
|
|
470
|
+
function customActionsDescription(actions) {
|
|
471
|
+
return ` Custom actions: ${actions.map((r) => `${r.name} (${r.description})`).join('; ')}.`;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Merge custom actions into the existing `superdoc_perform_action` tool: append names to
|
|
475
|
+
* the `action` enum, union inputSchema.properties into the tool's properties,
|
|
476
|
+
* and extend the description.
|
|
477
|
+
*/
|
|
478
|
+
/**
|
|
479
|
+
* Reject a custom arg whose name collides with an existing arg (built-in or an
|
|
480
|
+
* earlier custom action) of a DIFFERENT shape. Merging into one flat
|
|
481
|
+
* `superdoc_perform_action` schema means a single name → one schema; silently
|
|
482
|
+
* keeping the first would advertise one shape for two meanings. Identical
|
|
483
|
+
* re-declarations (same JSON Schema) are allowed — actions may share an arg.
|
|
484
|
+
*/
|
|
485
|
+
/** Order-insensitive JSON serialization: object keys sorted recursively (array
|
|
486
|
+
* order preserved — it is semantic for `enum`/`required`). So two schemas that
|
|
487
|
+
* differ ONLY in key order compare equal. */
|
|
488
|
+
function canonicalJson(value) {
|
|
489
|
+
if (value === null || typeof value !== 'object')
|
|
490
|
+
return JSON.stringify(value) ?? 'null';
|
|
491
|
+
if (Array.isArray(value))
|
|
492
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
493
|
+
const obj = value;
|
|
494
|
+
const body = Object.keys(obj)
|
|
495
|
+
.sort()
|
|
496
|
+
.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`)
|
|
497
|
+
.join(',');
|
|
498
|
+
return `{${body}}`;
|
|
499
|
+
}
|
|
500
|
+
/** Documentation-only JSON Schema keywords — differences here never conflict. */
|
|
501
|
+
const METADATA_SCHEMA_KEYS = new Set(['description', 'title', 'examples', '$comment']);
|
|
502
|
+
/** Drop doc-only keys (recursively) so comparison sees just the structural shape. */
|
|
503
|
+
function structuralSchema(value) {
|
|
504
|
+
if (value === null || typeof value !== 'object')
|
|
505
|
+
return value;
|
|
506
|
+
if (Array.isArray(value))
|
|
507
|
+
return value.map(structuralSchema);
|
|
508
|
+
const out = {};
|
|
509
|
+
for (const [k, v] of Object.entries(value)) {
|
|
510
|
+
if (METADATA_SCHEMA_KEYS.has(k))
|
|
511
|
+
continue;
|
|
512
|
+
out[k] = structuralSchema(v);
|
|
513
|
+
}
|
|
514
|
+
return out;
|
|
515
|
+
}
|
|
516
|
+
/** Two arg schemas CONFLICT when they differ in any way EXCEPT documentation
|
|
517
|
+
* (description/title/examples/$comment): reusing a built-in arg name with your
|
|
518
|
+
* own description is fine, but a different type, enum (incl. one-sided),
|
|
519
|
+
* default, limit, pattern, or nested shape is a real conflict — the merged
|
|
520
|
+
* surface advertises ONE schema per name. */
|
|
521
|
+
function argSchemasConflict(a, b) {
|
|
522
|
+
return canonicalJson(structuralSchema(a)) !== canonicalJson(structuralSchema(b));
|
|
523
|
+
}
|
|
524
|
+
function assertNoArgConflict(properties, key, value, actionName) {
|
|
525
|
+
if (key in properties && argSchemasConflict(properties[key], value)) {
|
|
526
|
+
throw new errors.SuperDocCliError(`Custom action "${actionName}" declares argument "${key}" with a schema that conflicts with an existing ` +
|
|
527
|
+
`argument of the same name on the superdoc_perform_action surface (they differ beyond description). Rename the argument, or match the existing schema exactly.`, { code: 'INVALID_ARGUMENT', details: { action: actionName, arg: key } });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function mergeIntoAgentAction(tools, actions) {
|
|
531
|
+
return tools.map((tool) => {
|
|
532
|
+
if (toolNameOf(tool) !== 'superdoc_perform_action')
|
|
533
|
+
return tool;
|
|
534
|
+
const t = tool;
|
|
535
|
+
// Provider dialects: openai nests under `function` (`parameters`);
|
|
536
|
+
// anthropic is flat with `input_schema`; the core agent dialect for
|
|
537
|
+
// vercel is flat with `inputSchema`; generic is flat with `parameters`.
|
|
538
|
+
const fn = isRecord(t.function) ? t.function : null;
|
|
539
|
+
const schemaContainer = fn ?? t;
|
|
540
|
+
const schemaKey = 'input_schema' in schemaContainer
|
|
541
|
+
? 'input_schema'
|
|
542
|
+
: 'inputSchema' in schemaContainer
|
|
543
|
+
? 'inputSchema'
|
|
544
|
+
: 'parameters';
|
|
545
|
+
const schema = isRecord(schemaContainer[schemaKey]) ? schemaContainer[schemaKey] : {};
|
|
546
|
+
const properties = isRecord(schema.properties) ? { ...schema.properties } : {};
|
|
547
|
+
const actionProp = isRecord(properties.action) ? { ...properties.action } : {};
|
|
548
|
+
const enumValues = Array.isArray(actionProp.enum) ? [...actionProp.enum] : [];
|
|
549
|
+
for (const action of actions) {
|
|
550
|
+
if (!enumValues.includes(action.name))
|
|
551
|
+
enumValues.push(action.name);
|
|
552
|
+
for (const [key, value] of Object.entries(action.inputSchema.properties ?? {})) {
|
|
553
|
+
assertNoArgConflict(properties, key, value, action.name);
|
|
554
|
+
if (!(key in properties))
|
|
555
|
+
properties[key] = value;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const nextActionProp = { ...actionProp, enum: enumValues };
|
|
559
|
+
const nextSchema = { ...schema, properties: { ...properties, action: nextActionProp } };
|
|
560
|
+
const baseDescription = typeof schemaContainer.description === 'string' ? schemaContainer.description : '';
|
|
561
|
+
const nextDescription = baseDescription + customActionsDescription(actions);
|
|
562
|
+
const nextContainer = { ...schemaContainer, description: nextDescription, [schemaKey]: nextSchema };
|
|
563
|
+
return fn ? { ...t, function: nextContainer } : nextContainer;
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* When the base dropped `superdoc_perform_action` entirely (all built-ins
|
|
568
|
+
* excluded / an empty allowlist), active custom actions would be advertised in
|
|
569
|
+
* the prompt and dispatchable — but carried by NO tool. Synthesize a
|
|
570
|
+
* custom-only definition so a curated custom-only preset stays callable.
|
|
571
|
+
*/
|
|
572
|
+
function synthesizePerformAction(provider, actions) {
|
|
573
|
+
const properties = {
|
|
574
|
+
action: { type: 'string', enum: actions.map((action) => action.name) },
|
|
575
|
+
};
|
|
576
|
+
for (const action of actions) {
|
|
577
|
+
for (const [key, value] of Object.entries(action.inputSchema.properties ?? {})) {
|
|
578
|
+
assertNoArgConflict(properties, key, value, action.name);
|
|
579
|
+
if (!(key in properties))
|
|
580
|
+
properties[key] = value;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const schema = { type: 'object', additionalProperties: true, required: ['action'], properties };
|
|
584
|
+
const description = "Perform one of this preset's custom document actions. Pick an action and pass its flat arguments." +
|
|
585
|
+
customActionsDescription(actions);
|
|
586
|
+
if (provider === 'anthropic') {
|
|
587
|
+
return { name: 'superdoc_perform_action', description, input_schema: schema };
|
|
588
|
+
}
|
|
589
|
+
if (provider === 'vercel') {
|
|
590
|
+
return { name: 'superdoc_perform_action', description, inputSchema: schema };
|
|
591
|
+
}
|
|
592
|
+
if (provider === 'openai') {
|
|
593
|
+
return { type: 'function', function: { name: 'superdoc_perform_action', description, parameters: schema } };
|
|
594
|
+
}
|
|
595
|
+
return { name: 'superdoc_perform_action', description, parameters: schema };
|
|
596
|
+
}
|
|
597
|
+
/** Merge into an existing perform_action tool, or synthesize one if the base dropped it. */
|
|
598
|
+
function mergeOrSynthesizePerformAction(tools, actions, provider) {
|
|
599
|
+
const hasPerformAction = tools.some((tool) => toolNameOf(tool) === 'superdoc_perform_action');
|
|
600
|
+
if (hasPerformAction)
|
|
601
|
+
return mergeIntoAgentAction(tools, actions);
|
|
602
|
+
return [...tools, synthesizePerformAction(provider, actions)];
|
|
603
|
+
}
|
|
604
|
+
/** Build a single provider-shaped standalone tool for a custom action. */
|
|
605
|
+
function standaloneTool(provider, action) {
|
|
606
|
+
if (provider === 'anthropic') {
|
|
607
|
+
return { name: action.name, description: action.description, input_schema: action.inputSchema };
|
|
608
|
+
}
|
|
609
|
+
// The core agent dialect for vercel is FLAT {name, description, inputSchema}
|
|
610
|
+
// (agent/catalog.ts toVercelTool) — not the OpenAI nested function shape.
|
|
611
|
+
if (provider === 'vercel') {
|
|
612
|
+
return { name: action.name, description: action.description, inputSchema: action.inputSchema };
|
|
613
|
+
}
|
|
614
|
+
if (provider === 'openai') {
|
|
615
|
+
return {
|
|
616
|
+
type: 'function',
|
|
617
|
+
function: { name: action.name, description: action.description, parameters: action.inputSchema },
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
// generic
|
|
621
|
+
return { name: action.name, description: action.description, parameters: action.inputSchema };
|
|
622
|
+
}
|
|
623
|
+
function autoSystemPromptSection(actions) {
|
|
624
|
+
if (actions.length === 0)
|
|
625
|
+
return '';
|
|
626
|
+
const bullets = actions.map((r) => `- ${r.name} — ${r.description}`).join('\n');
|
|
627
|
+
return `\n\n## Custom actions\n${bullets}`;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Wrap `getPreset(baseId)` with custom actions. Returns a new
|
|
631
|
+
* {@link PresetDescriptor} that advertises and dispatches the custom actions
|
|
632
|
+
* while delegating everything else to the base preset. With
|
|
633
|
+
* `includeActions`, the base's built-in surface is narrowed to that allowlist.
|
|
634
|
+
*/
|
|
635
|
+
function extendPreset(baseId, options) {
|
|
636
|
+
const base = presets.getPreset(baseId);
|
|
637
|
+
// Snapshot the caller's array — the preset surface must stay immutable even
|
|
638
|
+
// if discovery/hot-reload code mutates the original list after construction
|
|
639
|
+
// (else getTools/prompt would drift from the byName dispatch map).
|
|
640
|
+
const actions$1 = options.actions ? [...options.actions] : [];
|
|
641
|
+
const standalone = options.standalone === true;
|
|
642
|
+
assertActionsValid(actions$1, options.id, standalone);
|
|
643
|
+
const byName = new Map(actions$1.map((r) => [r.name, r]));
|
|
644
|
+
const include = options.includeActions != null ? new Set(options.includeActions) : null;
|
|
645
|
+
if (include) {
|
|
646
|
+
for (const name of include) {
|
|
647
|
+
if (!BUILTIN_ACTION_NAMES.has(name)) {
|
|
648
|
+
throw new errors.SuperDocCliError(`includeActions: unknown action "${name}".`, {
|
|
649
|
+
code: 'INVALID_ARGUMENT',
|
|
650
|
+
details: { presetId: options.id, unknownAction: name },
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// The allowlist is implemented as a DERIVED exclusion forwarded to the
|
|
656
|
+
// base, so core narrows the enum, the grouped description, AND the
|
|
657
|
+
// advertised argument properties natively — no hand-rebuilt schemas here
|
|
658
|
+
// (a second implementation of that narrowing is exactly what drifts).
|
|
659
|
+
const derivedExcludes = include ? actions.ACTION_NAMES_LIST.filter((name) => !include.has(name)) : [];
|
|
660
|
+
const splitExclusions = (list) => splitCustomExclusions(byName, list);
|
|
661
|
+
/** Built-in exclusions the base applies: allowlist-derived ∪ requested. */
|
|
662
|
+
const builtinExcludesFor = (requested) => {
|
|
663
|
+
const merged = [...new Set([...derivedExcludes, ...(requested ?? [])])];
|
|
664
|
+
return merged.length > 0 ? merged : undefined;
|
|
665
|
+
};
|
|
666
|
+
async function getTools(provider, toolOptions) {
|
|
667
|
+
const { customExcluded, builtinExcluded } = splitExclusions(toolOptions?.excludeActions);
|
|
668
|
+
const excludeActions = builtinExcludesFor(builtinExcluded);
|
|
669
|
+
const baseOptions = toolOptions || excludeActions ? { ...toolOptions, excludeActions } : undefined;
|
|
670
|
+
const result = await base.getTools(provider, baseOptions);
|
|
671
|
+
const activeActions = actions$1.filter((action) => !customExcluded.has(action.name));
|
|
672
|
+
if (activeActions.length === 0)
|
|
673
|
+
return result;
|
|
674
|
+
// Only re-place the anthropic marker when the base actually applied one —
|
|
675
|
+
// marking tools for a base that reported 'disabled' would make the marker
|
|
676
|
+
// and the cacheStrategy metadata disagree.
|
|
677
|
+
const cacheRequested = toolOptions?.cache === true && result.cacheStrategy !== 'disabled';
|
|
678
|
+
if (standalone) {
|
|
679
|
+
const extra = activeActions.map((action) => standaloneTool(provider, action));
|
|
680
|
+
const tools = renormalizeAnthropicCacheMarker([...result.tools, ...extra], provider, cacheRequested);
|
|
681
|
+
return { ...result, tools };
|
|
682
|
+
}
|
|
683
|
+
// Merge keeps the same tool count/order, so the marker is unaffected — but
|
|
684
|
+
// re-normalize anyway to stay correct if a base ever reorders or drops the
|
|
685
|
+
// perform_action tool (every built-in excluded → synthesized custom-only).
|
|
686
|
+
const merged = renormalizeAnthropicCacheMarker(mergeOrSynthesizePerformAction(result.tools, activeActions, provider), provider, cacheRequested);
|
|
687
|
+
return { ...result, tools: merged };
|
|
688
|
+
}
|
|
689
|
+
async function getCatalog() {
|
|
690
|
+
const catalog$1 = await base.getCatalog();
|
|
691
|
+
let rows = catalog$1.tools;
|
|
692
|
+
// Advertised == dispatchable, catalog included: when includeActions
|
|
693
|
+
// narrows the surface, the catalog's superdoc_perform_action row must narrow
|
|
694
|
+
// WITH it — enum, description, AND argument properties — or getToolCatalog()
|
|
695
|
+
// still advertises inputs for actions the preset refuses. Rebuild the row
|
|
696
|
+
// from the SAME builder getTools drives through the base (buildPerform-
|
|
697
|
+
// ActionDefinition), so there is one narrowing implementation, not a second
|
|
698
|
+
// that drifts. Canonical ACTION_NAMES_LIST order matches the getTools row.
|
|
699
|
+
if (include) {
|
|
700
|
+
const includedBuiltins = actions.ACTION_NAMES_LIST.filter((name) => include.has(name));
|
|
701
|
+
if (includedBuiltins.length === 0) {
|
|
702
|
+
rows = rows.filter((row) => row.toolName !== 'superdoc_perform_action');
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
const def = catalog.buildPerformActionDefinition(includedBuiltins);
|
|
706
|
+
rows = rows.map((row) => row.toolName === 'superdoc_perform_action'
|
|
707
|
+
? {
|
|
708
|
+
...row,
|
|
709
|
+
description: def.description,
|
|
710
|
+
inputSchema: def.inputSchema,
|
|
711
|
+
}
|
|
712
|
+
: row);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const extraRows = actions$1.map((action) => ({
|
|
716
|
+
toolName: action.name,
|
|
717
|
+
description: action.description,
|
|
718
|
+
inputSchema: action.inputSchema,
|
|
719
|
+
mutates: true,
|
|
720
|
+
operations: [],
|
|
721
|
+
}));
|
|
722
|
+
const tools = [...rows, ...extraRows];
|
|
723
|
+
return { ...catalog$1, toolCount: tools.length, tools };
|
|
724
|
+
}
|
|
725
|
+
async function getSystemPrompt(promptOptions) {
|
|
726
|
+
// includeActions narrows the ENUM; the prompt must narrow WITH it — a
|
|
727
|
+
// per-action manual for an uncallable action teaches the model to call it.
|
|
728
|
+
const { customExcluded, builtinExcluded } = splitExclusions(promptOptions?.excludeActions);
|
|
729
|
+
const excludeActions = builtinExcludesFor(builtinExcluded);
|
|
730
|
+
const basePrompt = await base.getSystemPrompt(excludeActions ? { ...promptOptions, excludeActions } : undefined);
|
|
731
|
+
const activeActions = actions$1.filter((action) => !customExcluded.has(action.name));
|
|
732
|
+
const extra = options.systemPromptExtra ?? autoSystemPromptSection(activeActions);
|
|
733
|
+
return basePrompt + extra;
|
|
734
|
+
}
|
|
735
|
+
async function getMcpPrompt() {
|
|
736
|
+
const basePrompt = await base.getMcpPrompt();
|
|
737
|
+
const extra = options.systemPromptExtra ?? autoSystemPromptSection(actions$1);
|
|
738
|
+
return basePrompt + extra;
|
|
739
|
+
}
|
|
740
|
+
async function dispatch(documentHandle, toolName, args, invokeOptions) {
|
|
741
|
+
// Defense-in-depth parity with core: a host that narrowed the advertised
|
|
742
|
+
// surface passes the same exclusions here, and an excluded CUSTOM action
|
|
743
|
+
// must be refused before it runs (the base can only refuse built-ins).
|
|
744
|
+
const exclusionOptions = invokeOptions;
|
|
745
|
+
const excluded = new Set(exclusionOptions?.excludeActions ?? []);
|
|
746
|
+
if (toolName === 'superdoc_perform_action' && isRecord(args) && typeof args.action === 'string') {
|
|
747
|
+
// Advertised surface == dispatchable surface: in standalone mode custom
|
|
748
|
+
// actions are advertised as their own tools, so the (unadvertised)
|
|
749
|
+
// perform_action route must not execute them — it falls through to the
|
|
750
|
+
// base, which rejects the unknown action name.
|
|
751
|
+
if (!standalone) {
|
|
752
|
+
const action = byName.get(args.action);
|
|
753
|
+
if (action) {
|
|
754
|
+
if (excluded.has(action.name))
|
|
755
|
+
throwExcludedAction(toolName, action.name);
|
|
756
|
+
return runCustomAction(base, action, documentHandle, args, invokeOptions);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
// The allowlist is a dispatch boundary too: a built-in outside
|
|
760
|
+
// includeActions is not advertised and must not execute on a
|
|
761
|
+
// guessed/stale call (defense-in-depth parity with excludeActions).
|
|
762
|
+
if (include && BUILTIN_ACTION_NAMES.has(args.action) && !include.has(args.action)) {
|
|
763
|
+
throwExcludedAction(toolName, args.action, { excludedBy: 'includeActions' });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (standalone && byName.has(toolName)) {
|
|
767
|
+
if (excluded.has(toolName))
|
|
768
|
+
throwExcludedAction(toolName, toolName);
|
|
769
|
+
const action = byName.get(toolName);
|
|
770
|
+
return runCustomAction(base, action, documentHandle, args, invokeOptions, /* fromPerformAction */ false);
|
|
771
|
+
}
|
|
772
|
+
return base.dispatch(documentHandle, toolName, args, invokeOptions);
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
id: options.id,
|
|
776
|
+
description: options.description ?? `${base.description} + ${actions$1.length} custom action(s).`,
|
|
777
|
+
supportsCacheControl: base.supportsCacheControl,
|
|
778
|
+
getTools,
|
|
779
|
+
getCatalog,
|
|
780
|
+
getSystemPrompt,
|
|
781
|
+
getMcpPrompt,
|
|
782
|
+
dispatch,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
exports.defineAction = defineAction;
|
|
787
|
+
exports.executionKindOf = executionKindOf;
|
|
788
|
+
exports.extendPreset = extendPreset;
|