@khanglvm/outline-cli 0.1.3 → 0.1.4
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/CHANGELOG.md +5 -0
- package/README.md +34 -13
- package/bin/outline-cli.js +14 -0
- package/docs/TOOL_CONTRACTS.md +8 -0
- package/package.json +1 -1
- package/src/agent-skills.js +15 -15
- package/src/cli.js +203 -62
- package/src/config-store.js +86 -6
- package/src/entry-integrity-manifest.generated.js +15 -11
- package/src/entry-integrity.js +3 -0
- package/src/summary-redaction.js +37 -0
- package/src/tool-arg-schemas.js +266 -10
- package/src/tools.extended.js +123 -16
- package/src/tools.js +277 -21
- package/src/tools.mutation.js +2 -1
- package/src/tools.navigation.js +3 -2
- package/test/agent-skills.unit.test.js +2 -2
- package/test/config-store.unit.test.js +32 -0
- package/test/hardening.unit.test.js +26 -1
- package/test/live.integration.test.js +20 -24
- package/test/profile-selection.unit.test.js +14 -4
- package/test/tool-resolution.unit.test.js +333 -0
- package/test/version.unit.test.js +21 -0
package/src/tool-arg-schemas.js
CHANGED
|
@@ -11,12 +11,8 @@ const TYPES = {
|
|
|
11
11
|
"string|string[]": (v) => typeof v === "string" || (Array.isArray(v) && v.every((x) => typeof x === "string")),
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
function fail(tool, issues) {
|
|
15
|
-
throw new CliError(`Invalid args for ${tool}`,
|
|
16
|
-
code: "ARG_VALIDATION_FAILED",
|
|
17
|
-
tool,
|
|
18
|
-
issues,
|
|
19
|
-
});
|
|
14
|
+
function fail(tool, issues, spec = {}, args = undefined) {
|
|
15
|
+
throw new CliError(`Invalid args for ${tool}`, buildValidationDetails(tool, spec, issues, args));
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
function ensureObject(tool, args) {
|
|
@@ -25,7 +21,248 @@ function ensureObject(tool, args) {
|
|
|
25
21
|
}
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
function
|
|
24
|
+
function levenshteinDistance(a, b) {
|
|
25
|
+
const left = String(a || "");
|
|
26
|
+
const right = String(b || "");
|
|
27
|
+
const rows = left.length + 1;
|
|
28
|
+
const cols = right.length + 1;
|
|
29
|
+
const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < rows; i += 1) {
|
|
32
|
+
matrix[i][0] = i;
|
|
33
|
+
}
|
|
34
|
+
for (let j = 0; j < cols; j += 1) {
|
|
35
|
+
matrix[0][j] = j;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (let i = 1; i < rows; i += 1) {
|
|
39
|
+
for (let j = 1; j < cols; j += 1) {
|
|
40
|
+
const cost = left[i - 1] === right[j - 1] ? 0 : 1;
|
|
41
|
+
matrix[i][j] = Math.min(
|
|
42
|
+
matrix[i - 1][j] + 1,
|
|
43
|
+
matrix[i][j - 1] + 1,
|
|
44
|
+
matrix[i - 1][j - 1] + cost
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return matrix[rows - 1][cols - 1];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildAcceptedArgList(spec = {}) {
|
|
53
|
+
const accepted = new Set(Object.keys(spec.properties || {}));
|
|
54
|
+
accepted.add("compact");
|
|
55
|
+
return [...accepted].sort();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function suggestClosestArgNames(key, acceptedArgs) {
|
|
59
|
+
const raw = String(key || "").trim();
|
|
60
|
+
if (!raw) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return acceptedArgs
|
|
65
|
+
.map((candidate) => ({
|
|
66
|
+
candidate,
|
|
67
|
+
distance: levenshteinDistance(raw.toLowerCase(), String(candidate).toLowerCase()),
|
|
68
|
+
}))
|
|
69
|
+
.filter((row) => row.distance <= 3)
|
|
70
|
+
.sort((a, b) => a.distance - b.distance || a.candidate.localeCompare(b.candidate))
|
|
71
|
+
.slice(0, 3)
|
|
72
|
+
.map((row) => row.candidate);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function enrichIssuesWithArgSuggestions(issues, acceptedArgs) {
|
|
76
|
+
return issues.map((issue) => {
|
|
77
|
+
if (!issue || issue.message !== "is not allowed" || typeof issue.path !== "string") {
|
|
78
|
+
return issue;
|
|
79
|
+
}
|
|
80
|
+
const key = issue.path.startsWith("args.") ? issue.path.slice(5) : issue.path;
|
|
81
|
+
const suggestions = suggestClosestArgNames(key, acceptedArgs);
|
|
82
|
+
return suggestions.length > 0 ? { ...issue, suggestions } : issue;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractArgKeyFromIssue(issue) {
|
|
87
|
+
if (!issue || typeof issue.path !== "string") {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return issue.path.startsWith("args.") ? issue.path.slice(5) : issue.path;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function applySuggestedArgFixes(args, issues) {
|
|
94
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const next = { ...args };
|
|
99
|
+
let changed = false;
|
|
100
|
+
|
|
101
|
+
for (const issue of issues) {
|
|
102
|
+
if (!issue || issue.message !== "is not allowed") {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const key = extractArgKeyFromIssue(issue);
|
|
106
|
+
const target = Array.isArray(issue.suggestions) && issue.suggestions.length > 0
|
|
107
|
+
? issue.suggestions[0]
|
|
108
|
+
: null;
|
|
109
|
+
if (!key || !target || !(key in next) || key === target) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (target in next && next[target] !== next[key]) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!(target in next)) {
|
|
116
|
+
next[target] = next[key];
|
|
117
|
+
}
|
|
118
|
+
delete next[key];
|
|
119
|
+
changed = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return changed ? next : undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildSuggestedArgs(spec, args, enrichedIssues) {
|
|
126
|
+
const candidate = applySuggestedArgFixes(args, enrichedIssues);
|
|
127
|
+
if (!candidate) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const normalized = normalizeArgsForSpec(candidate, spec);
|
|
132
|
+
const remainingIssues = collectValidationIssues(normalized, spec);
|
|
133
|
+
return remainingIssues.length === 0 ? normalized : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildValidationDetails(tool, spec, issues, args = undefined) {
|
|
137
|
+
const acceptedArgs = buildAcceptedArgList(spec);
|
|
138
|
+
const enrichedIssues = enrichIssuesWithArgSuggestions(issues, acceptedArgs);
|
|
139
|
+
const requiredArgs = [...new Set(spec.required || [])].sort();
|
|
140
|
+
const unknownArgs = enrichedIssues
|
|
141
|
+
.filter((issue) => issue?.message === "is not allowed" && typeof issue.path === "string")
|
|
142
|
+
.map((issue) => issue.path.replace(/^args\./, ""));
|
|
143
|
+
const suggestedArgs = buildSuggestedArgs(spec, args, enrichedIssues);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
code: "ARG_VALIDATION_FAILED",
|
|
147
|
+
tool,
|
|
148
|
+
issues: enrichedIssues,
|
|
149
|
+
acceptedArgs,
|
|
150
|
+
requiredArgs,
|
|
151
|
+
unknownArgs,
|
|
152
|
+
suggestedArgs,
|
|
153
|
+
validationHint:
|
|
154
|
+
acceptedArgs.length > 0 ? `Accepted args: ${acceptedArgs.join(", ")}` : undefined,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function looksNumeric(value) {
|
|
159
|
+
return /^-?(?:\d+|\d+\.\d+)$/.test(String(value || "").trim());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function coerceScalarValue(types, value) {
|
|
163
|
+
if (typeof value !== "string") {
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const trimmed = value.trim();
|
|
168
|
+
if (!trimmed) {
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (types.includes("boolean") && /^(true|false)$/i.test(trimmed)) {
|
|
173
|
+
return trimmed.toLowerCase() === "true";
|
|
174
|
+
}
|
|
175
|
+
if (types.includes("null") && /^null$/i.test(trimmed)) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
if (types.includes("number") && looksNumeric(trimmed)) {
|
|
179
|
+
return Number(trimmed);
|
|
180
|
+
}
|
|
181
|
+
if (types.includes("object") && /^[{]/.test(trimmed)) {
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(trimmed);
|
|
184
|
+
} catch {
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (types.includes("array") && /^[[]/.test(trimmed)) {
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(trimmed);
|
|
191
|
+
} catch {
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function coerceArrayValue(types, value) {
|
|
200
|
+
if (!Array.isArray(value)) {
|
|
201
|
+
if (types.includes("string[]") && typeof value === "string") {
|
|
202
|
+
return [value];
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (types.includes("string[]")) {
|
|
208
|
+
return value.map((item) => (typeof item === "string" ? item : String(item)));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function normalizeCrossFieldAliases(args, properties) {
|
|
215
|
+
const next = { ...args };
|
|
216
|
+
|
|
217
|
+
if (Array.isArray(next.query) && properties.queries && next.queries === undefined) {
|
|
218
|
+
next.queries = next.query.map((item) => String(item));
|
|
219
|
+
delete next.query;
|
|
220
|
+
}
|
|
221
|
+
if (Array.isArray(next.question) && properties.questions && next.questions === undefined) {
|
|
222
|
+
next.questions = next.question.map((item) => String(item));
|
|
223
|
+
delete next.question;
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(next.id) && properties.ids && next.ids === undefined) {
|
|
226
|
+
next.ids = next.id.map((item) => String(item));
|
|
227
|
+
delete next.id;
|
|
228
|
+
}
|
|
229
|
+
if (typeof next.ids === "string" && properties.id && !properties.ids && next.id === undefined) {
|
|
230
|
+
next.id = next.ids;
|
|
231
|
+
delete next.ids;
|
|
232
|
+
}
|
|
233
|
+
if (typeof next.queries === "string" && properties.query && !properties.queries && next.query === undefined) {
|
|
234
|
+
next.query = next.queries;
|
|
235
|
+
delete next.queries;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return next;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizeArgsForSpec(args, spec) {
|
|
242
|
+
const properties = spec.properties || {};
|
|
243
|
+
const next = normalizeCrossFieldAliases(args, properties);
|
|
244
|
+
|
|
245
|
+
for (const [key, rule] of Object.entries(properties)) {
|
|
246
|
+
if (!(key in next)) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const types = Array.isArray(rule.type) ? rule.type : [rule.type];
|
|
251
|
+
let value = next[key];
|
|
252
|
+
value = coerceArrayValue(types, value);
|
|
253
|
+
value = coerceScalarValue(types, value);
|
|
254
|
+
|
|
255
|
+
if (Array.isArray(value) && types.includes("string[]")) {
|
|
256
|
+
value = value.map((item) => (typeof item === "string" ? item : String(item)));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
next[key] = value;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return next;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function collectValidationIssues(args, spec) {
|
|
29
266
|
const issues = [];
|
|
30
267
|
const properties = spec.properties || {};
|
|
31
268
|
|
|
@@ -81,8 +318,14 @@ function validateSpec(tool, args, spec) {
|
|
|
81
318
|
spec.custom(args, issues);
|
|
82
319
|
}
|
|
83
320
|
|
|
321
|
+
return issues;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function validateSpec(tool, args, spec) {
|
|
325
|
+
const issues = collectValidationIssues(args, spec);
|
|
326
|
+
|
|
84
327
|
if (issues.length > 0) {
|
|
85
|
-
fail(tool, issues);
|
|
328
|
+
fail(tool, issues, spec, args);
|
|
86
329
|
}
|
|
87
330
|
}
|
|
88
331
|
|
|
@@ -151,10 +394,19 @@ export const TOOL_ARG_SCHEMAS = {
|
|
|
151
394
|
sort: { type: "string" },
|
|
152
395
|
direction: { type: "string", enum: ["ASC", "DESC"] },
|
|
153
396
|
parentDocumentId: { type: ["string", "null"] },
|
|
397
|
+
rootOnly: { type: "boolean" },
|
|
154
398
|
backlinkDocumentId: { type: "string" },
|
|
155
399
|
includePolicies: { type: "boolean" },
|
|
156
400
|
...SHARED_DOC_COMMON,
|
|
157
401
|
},
|
|
402
|
+
custom(args, issues) {
|
|
403
|
+
if (args.rootOnly === true && Object.prototype.hasOwnProperty.call(args, "parentDocumentId") && args.parentDocumentId !== null) {
|
|
404
|
+
issues.push({
|
|
405
|
+
path: "args.rootOnly",
|
|
406
|
+
message: "cannot be combined with a non-null args.parentDocumentId",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
},
|
|
158
410
|
},
|
|
159
411
|
"documents.backlinks": {
|
|
160
412
|
required: ["id"],
|
|
@@ -2245,6 +2497,7 @@ export const TOOL_ARG_SCHEMAS = {
|
|
|
2245
2497
|
dateFilter: { type: "string", enum: ["day", "week", "month", "year"] },
|
|
2246
2498
|
includePolicies: { type: "boolean" },
|
|
2247
2499
|
includeEvidenceDocs: { type: "boolean" },
|
|
2500
|
+
limit: { type: "number", min: 1 },
|
|
2248
2501
|
view: { type: "string", enum: ["summary", "full"] },
|
|
2249
2502
|
maxAttempts: { type: "number", min: 1 },
|
|
2250
2503
|
},
|
|
@@ -2267,6 +2520,7 @@ export const TOOL_ARG_SCHEMAS = {
|
|
|
2267
2520
|
dateFilter: { type: "string", enum: ["day", "week", "month", "year"] },
|
|
2268
2521
|
includePolicies: { type: "boolean" },
|
|
2269
2522
|
includeEvidenceDocs: { type: "boolean" },
|
|
2523
|
+
limit: { type: "number", min: 1 },
|
|
2270
2524
|
view: { type: "string", enum: ["summary", "full"] },
|
|
2271
2525
|
concurrency: { type: "number", min: 1 },
|
|
2272
2526
|
maxAttempts: { type: "number", min: 1 },
|
|
@@ -2338,9 +2592,11 @@ export const TOOL_ARG_SCHEMAS = {
|
|
|
2338
2592
|
export function validateToolArgs(toolName, args = {}) {
|
|
2339
2593
|
const spec = TOOL_ARG_SCHEMAS[toolName];
|
|
2340
2594
|
if (!spec) {
|
|
2341
|
-
return;
|
|
2595
|
+
return args;
|
|
2342
2596
|
}
|
|
2343
2597
|
|
|
2344
2598
|
ensureObject(toolName, args);
|
|
2345
|
-
|
|
2599
|
+
const normalizedArgs = normalizeArgsForSpec(args, spec);
|
|
2600
|
+
validateSpec(toolName, normalizedArgs, spec);
|
|
2601
|
+
return normalizedArgs;
|
|
2346
2602
|
}
|
package/src/tools.extended.js
CHANGED
|
@@ -376,6 +376,87 @@ function parseQuestionItem(raw, index) {
|
|
|
376
376
|
throw new CliError(`questions[${index}] must be string or object`);
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
function isAnswerEndpointUnsupported(err) {
|
|
380
|
+
if (!(err instanceof ApiError)) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
if (err.details?.status !== 404) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
const url = String(err.details?.url || "").toLowerCase();
|
|
387
|
+
const message = String(err.message || "").toLowerCase();
|
|
388
|
+
return url.includes("documents.answerquestion") || message.includes("answerquestion");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function normalizeFallbackAnswerHit(row, contextChars = 220) {
|
|
392
|
+
const doc = row?.document || row || {};
|
|
393
|
+
const context = typeof row?.context === "string" ? row.context : "";
|
|
394
|
+
return compactValue({
|
|
395
|
+
id: doc.id,
|
|
396
|
+
title: doc.title,
|
|
397
|
+
collectionId: doc.collectionId,
|
|
398
|
+
parentDocumentId: doc.parentDocumentId,
|
|
399
|
+
updatedAt: doc.updatedAt,
|
|
400
|
+
publishedAt: doc.publishedAt,
|
|
401
|
+
urlId: doc.urlId,
|
|
402
|
+
ranking: row?.ranking,
|
|
403
|
+
context: context.length > contextChars ? `${context.slice(0, contextChars)}...` : context,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function buildAnswerFallbackResult(ctx, question, args = {}) {
|
|
408
|
+
const maxAttempts = Math.max(1, toInteger(args.maxAttempts, 2));
|
|
409
|
+
const contextChars = Math.max(80, toInteger(args.contextChars, 220));
|
|
410
|
+
const limit = Math.max(1, Math.min(8, toInteger(args.limit, 5)));
|
|
411
|
+
const body = compactValue({
|
|
412
|
+
query: question,
|
|
413
|
+
collectionId: args.collectionId,
|
|
414
|
+
documentId: args.documentId || args.id,
|
|
415
|
+
userId: args.userId,
|
|
416
|
+
shareId: args.shareId,
|
|
417
|
+
statusFilter: args.statusFilter,
|
|
418
|
+
limit,
|
|
419
|
+
offset: 0,
|
|
420
|
+
snippetMinWords: toInteger(args.snippetMinWords, 20),
|
|
421
|
+
snippetMaxWords: toInteger(args.snippetMaxWords, 30),
|
|
422
|
+
}) || {};
|
|
423
|
+
|
|
424
|
+
const res = await ctx.client.call("documents.search", body, { maxAttempts });
|
|
425
|
+
const payload = maybeDropPolicies(res.body, !!args.includePolicies);
|
|
426
|
+
const hits = Array.isArray(payload?.data) ? payload.data : [];
|
|
427
|
+
const documents = hits.slice(0, Math.min(5, limit)).map((row) => normalizeFallbackAnswerHit(row, contextChars));
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
question,
|
|
431
|
+
answer: "",
|
|
432
|
+
noAnswerReason: "documents.answerQuestion is unsupported by this Outline deployment; returning ranked retrieval results instead.",
|
|
433
|
+
unsupported: true,
|
|
434
|
+
fallbackUsed: true,
|
|
435
|
+
fallbackTool: "documents.search",
|
|
436
|
+
fallbackSuggestion: {
|
|
437
|
+
tool: "documents.search",
|
|
438
|
+
args: compactValue({
|
|
439
|
+
query: question,
|
|
440
|
+
collectionId: args.collectionId,
|
|
441
|
+
documentId: args.documentId || args.id,
|
|
442
|
+
userId: args.userId,
|
|
443
|
+
shareId: args.shareId,
|
|
444
|
+
statusFilter: args.statusFilter,
|
|
445
|
+
limit,
|
|
446
|
+
view: "summary",
|
|
447
|
+
}),
|
|
448
|
+
},
|
|
449
|
+
documents,
|
|
450
|
+
retrieval: {
|
|
451
|
+
query: question,
|
|
452
|
+
result: compactValue({
|
|
453
|
+
...payload,
|
|
454
|
+
data: hits.map((row) => normalizeFallbackAnswerHit(row, contextChars)),
|
|
455
|
+
}),
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
379
460
|
async function documentsAnswerTool(ctx, args = {}) {
|
|
380
461
|
const question = String(args.question ?? args.query ?? "").trim();
|
|
381
462
|
if (!question) {
|
|
@@ -383,23 +464,35 @@ async function documentsAnswerTool(ctx, args = {}) {
|
|
|
383
464
|
}
|
|
384
465
|
|
|
385
466
|
const body = {
|
|
386
|
-
...buildBody(args, ["question", "query"]),
|
|
467
|
+
...buildBody(args, ["question", "query", "limit"]),
|
|
387
468
|
query: question,
|
|
388
469
|
};
|
|
389
470
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
471
|
+
try {
|
|
472
|
+
const res = await ctx.client.call("documents.answerQuestion", body, {
|
|
473
|
+
maxAttempts: toInteger(args.maxAttempts, 2),
|
|
474
|
+
});
|
|
475
|
+
const payload = maybeDropPolicies(res.body, !!args.includePolicies);
|
|
394
476
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
477
|
+
return {
|
|
478
|
+
tool: "documents.answer",
|
|
479
|
+
profile: ctx.profile.id,
|
|
480
|
+
result:
|
|
481
|
+
payload && typeof payload === "object"
|
|
482
|
+
? { question, ...payload }
|
|
483
|
+
: { question, data: payload },
|
|
484
|
+
};
|
|
485
|
+
} catch (err) {
|
|
486
|
+
if (!isAnswerEndpointUnsupported(err)) {
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
tool: "documents.answer",
|
|
492
|
+
profile: ctx.profile.id,
|
|
493
|
+
result: await buildAnswerFallbackResult(ctx, question, args),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
403
496
|
}
|
|
404
497
|
|
|
405
498
|
async function documentsAnswerBatchTool(ctx, args = {}) {
|
|
@@ -415,7 +508,7 @@ async function documentsAnswerBatchTool(ctx, args = {}) {
|
|
|
415
508
|
throw new CliError("documents.answer_batch requires args.question or args.questions[]");
|
|
416
509
|
}
|
|
417
510
|
|
|
418
|
-
const baseBody = buildBody(args, ["question", "questions", "query", "concurrency"]);
|
|
511
|
+
const baseBody = buildBody(args, ["question", "questions", "query", "concurrency", "limit"]);
|
|
419
512
|
const includePolicies = !!args.includePolicies;
|
|
420
513
|
const maxAttempts = toInteger(args.maxAttempts, 2);
|
|
421
514
|
const concurrency = Math.max(1, Math.min(10, toInteger(args.concurrency, 3)));
|
|
@@ -441,6 +534,18 @@ async function documentsAnswerBatchTool(ctx, args = {}) {
|
|
|
441
534
|
result: payload,
|
|
442
535
|
};
|
|
443
536
|
} catch (err) {
|
|
537
|
+
if (parsed && isAnswerEndpointUnsupported(err)) {
|
|
538
|
+
return {
|
|
539
|
+
index,
|
|
540
|
+
ok: true,
|
|
541
|
+
question: parsed.question,
|
|
542
|
+
documentId: parsed.documentId,
|
|
543
|
+
result: await buildAnswerFallbackResult(ctx, parsed.question, {
|
|
544
|
+
...args,
|
|
545
|
+
...parsed.body,
|
|
546
|
+
}),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
444
549
|
if (err instanceof ApiError || err instanceof CliError) {
|
|
445
550
|
return {
|
|
446
551
|
index,
|
|
@@ -2972,7 +3077,7 @@ export const EXTENDED_TOOLS = {
|
|
|
2972
3077
|
...RPC_TOOLS,
|
|
2973
3078
|
"documents.answer": {
|
|
2974
3079
|
signature:
|
|
2975
|
-
"documents.answer(args: { question?: string; query?: string; ...endpointArgs; includePolicies?: boolean; maxAttempts?: number })",
|
|
3080
|
+
"documents.answer(args: { question?: string; query?: string; limit?: number; ...endpointArgs; includePolicies?: boolean; maxAttempts?: number })",
|
|
2976
3081
|
description: "Answer a question using Outline AI over the selected document scope.",
|
|
2977
3082
|
usageExample: {
|
|
2978
3083
|
tool: "documents.answer",
|
|
@@ -2984,12 +3089,13 @@ export const EXTENDED_TOOLS = {
|
|
|
2984
3089
|
bestPractices: [
|
|
2985
3090
|
"Use question text that is specific enough to resolve citations quickly.",
|
|
2986
3091
|
"Scope by collectionId or documentId to reduce latency and hallucination risk.",
|
|
3092
|
+
"If the deployment lacks documents.answerQuestion, this wrapper returns fallback retrieval evidence and a concrete search suggestion instead of a raw 404.",
|
|
2987
3093
|
],
|
|
2988
3094
|
handler: documentsAnswerTool,
|
|
2989
3095
|
},
|
|
2990
3096
|
"documents.answer_batch": {
|
|
2991
3097
|
signature:
|
|
2992
|
-
"documents.answer_batch(args: { question?: string; questions?: Array<string | { question?: string; query?: string; ...endpointArgs }>; ...endpointArgs; concurrency?: number; includePolicies?: boolean; maxAttempts?: number })",
|
|
3098
|
+
"documents.answer_batch(args: { question?: string; questions?: Array<string | { question?: string; query?: string; ...endpointArgs }>; limit?: number; ...endpointArgs; concurrency?: number; includePolicies?: boolean; maxAttempts?: number })",
|
|
2993
3099
|
description: "Run multiple documents.answerQuestion calls with per-item isolation.",
|
|
2994
3100
|
usageExample: {
|
|
2995
3101
|
tool: "documents.answer_batch",
|
|
@@ -3005,6 +3111,7 @@ export const EXTENDED_TOOLS = {
|
|
|
3005
3111
|
bestPractices: [
|
|
3006
3112
|
"Prefer small batches and low concurrency for predictable token and latency budgets.",
|
|
3007
3113
|
"Use per-item statuses to retry only failures.",
|
|
3114
|
+
"Unsupported answer endpoints degrade to per-item retrieval evidence rather than failing the whole batch.",
|
|
3008
3115
|
],
|
|
3009
3116
|
handler: documentsAnswerBatchTool,
|
|
3010
3117
|
},
|