la-machina-engine 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +511 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +511 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1261,6 +1261,130 @@ var ApiAuthSchema = import_zod.z.discriminatedUnion("type", [
|
|
|
1261
1261
|
}).strict(),
|
|
1262
1262
|
import_zod.z.object({ type: import_zod.z.literal("custom"), id: import_zod.z.string().min(1) }).strict()
|
|
1263
1263
|
]);
|
|
1264
|
+
var ApiPaginationModeEnum = import_zod.z.enum(["offset", "page", "cursor", "link-header"]);
|
|
1265
|
+
var ApiPaginationStopEnum = import_zod.z.enum([
|
|
1266
|
+
"empty-page",
|
|
1267
|
+
"total-reached",
|
|
1268
|
+
"missing-cursor",
|
|
1269
|
+
"missing-next-link"
|
|
1270
|
+
]);
|
|
1271
|
+
var PAGINATION_MAX_LIMIT = 1e3;
|
|
1272
|
+
var ApiPaginationSchema = import_zod.z.object({
|
|
1273
|
+
mode: ApiPaginationModeEnum,
|
|
1274
|
+
request: import_zod.z.object({
|
|
1275
|
+
limitParam: import_zod.z.string().min(1).optional(),
|
|
1276
|
+
offsetParam: import_zod.z.string().min(1).optional(),
|
|
1277
|
+
pageParam: import_zod.z.string().min(1).optional(),
|
|
1278
|
+
cursorParam: import_zod.z.string().min(1).optional(),
|
|
1279
|
+
maxLimit: import_zod.z.number().int().positive().max(PAGINATION_MAX_LIMIT).optional(),
|
|
1280
|
+
firstPage: import_zod.z.number().int().nonnegative().optional()
|
|
1281
|
+
}).strict().optional(),
|
|
1282
|
+
response: import_zod.z.object({
|
|
1283
|
+
itemsPath: import_zod.z.string().min(1).optional(),
|
|
1284
|
+
totalPath: import_zod.z.string().min(1).optional(),
|
|
1285
|
+
nextCursorPath: import_zod.z.string().min(1).optional(),
|
|
1286
|
+
nextLinkPath: import_zod.z.string().min(1).optional()
|
|
1287
|
+
}).strict().optional(),
|
|
1288
|
+
stop: import_zod.z.object({
|
|
1289
|
+
strategy: ApiPaginationStopEnum
|
|
1290
|
+
}).strict().optional()
|
|
1291
|
+
}).strict().superRefine((p, ctx) => {
|
|
1292
|
+
const req2 = p.request ?? {};
|
|
1293
|
+
const res = p.response ?? {};
|
|
1294
|
+
if (p.mode === "offset") {
|
|
1295
|
+
if (req2.limitParam === void 0) {
|
|
1296
|
+
ctx.addIssue({
|
|
1297
|
+
code: "custom",
|
|
1298
|
+
message: "pagination.mode=offset requires request.limitParam",
|
|
1299
|
+
path: ["request", "limitParam"]
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
if (req2.offsetParam === void 0) {
|
|
1303
|
+
ctx.addIssue({
|
|
1304
|
+
code: "custom",
|
|
1305
|
+
message: "pagination.mode=offset requires request.offsetParam",
|
|
1306
|
+
path: ["request", "offsetParam"]
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
if (req2.maxLimit === void 0) {
|
|
1310
|
+
ctx.addIssue({
|
|
1311
|
+
code: "custom",
|
|
1312
|
+
message: "pagination.mode=offset requires request.maxLimit",
|
|
1313
|
+
path: ["request", "maxLimit"]
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
if (res.itemsPath === void 0) {
|
|
1317
|
+
ctx.addIssue({
|
|
1318
|
+
code: "custom",
|
|
1319
|
+
message: "pagination.mode=offset requires response.itemsPath",
|
|
1320
|
+
path: ["response", "itemsPath"]
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
} else if (p.mode === "page") {
|
|
1324
|
+
if (req2.pageParam === void 0) {
|
|
1325
|
+
ctx.addIssue({
|
|
1326
|
+
code: "custom",
|
|
1327
|
+
message: "pagination.mode=page requires request.pageParam",
|
|
1328
|
+
path: ["request", "pageParam"]
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
if (req2.maxLimit === void 0) {
|
|
1332
|
+
ctx.addIssue({
|
|
1333
|
+
code: "custom",
|
|
1334
|
+
message: "pagination.mode=page requires request.maxLimit",
|
|
1335
|
+
path: ["request", "maxLimit"]
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
if (res.itemsPath === void 0) {
|
|
1339
|
+
ctx.addIssue({
|
|
1340
|
+
code: "custom",
|
|
1341
|
+
message: "pagination.mode=page requires response.itemsPath",
|
|
1342
|
+
path: ["response", "itemsPath"]
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
} else if (p.mode === "cursor") {
|
|
1346
|
+
if (req2.cursorParam === void 0) {
|
|
1347
|
+
ctx.addIssue({
|
|
1348
|
+
code: "custom",
|
|
1349
|
+
message: "pagination.mode=cursor requires request.cursorParam",
|
|
1350
|
+
path: ["request", "cursorParam"]
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
if (res.nextCursorPath === void 0) {
|
|
1354
|
+
ctx.addIssue({
|
|
1355
|
+
code: "custom",
|
|
1356
|
+
message: "pagination.mode=cursor requires response.nextCursorPath",
|
|
1357
|
+
path: ["response", "nextCursorPath"]
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
if (res.itemsPath === void 0) {
|
|
1361
|
+
ctx.addIssue({
|
|
1362
|
+
code: "custom",
|
|
1363
|
+
message: "pagination.mode=cursor requires response.itemsPath",
|
|
1364
|
+
path: ["response", "itemsPath"]
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
} else if (p.mode === "link-header") {
|
|
1368
|
+
if (res.nextLinkPath === void 0) {
|
|
1369
|
+
ctx.addIssue({
|
|
1370
|
+
code: "custom",
|
|
1371
|
+
message: "pagination.mode=link-header requires response.nextLinkPath (HTTP Link header parsing is deferred)",
|
|
1372
|
+
path: ["response", "nextLinkPath"]
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
if (res.itemsPath === void 0) {
|
|
1376
|
+
ctx.addIssue({
|
|
1377
|
+
code: "custom",
|
|
1378
|
+
message: "pagination.mode=link-header requires response.itemsPath",
|
|
1379
|
+
path: ["response", "itemsPath"]
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
var ApiResponseMappingSchema = import_zod.z.object({
|
|
1385
|
+
itemsPath: import_zod.z.string().min(1).optional(),
|
|
1386
|
+
totalPath: import_zod.z.string().min(1).optional()
|
|
1387
|
+
}).strict();
|
|
1264
1388
|
var ApiEndpointSchema = import_zod.z.object({
|
|
1265
1389
|
method: ApiHttpMethodEnum,
|
|
1266
1390
|
path: import_zod.z.string().regex(/^\//, "path must start with /"),
|
|
@@ -1269,7 +1393,10 @@ var ApiEndpointSchema = import_zod.z.object({
|
|
|
1269
1393
|
// time by higher-level tooling (nikaido's yaml compiler), not
|
|
1270
1394
|
// here.
|
|
1271
1395
|
inputSchema: import_zod.z.unknown().optional(),
|
|
1272
|
-
outputHint: import_zod.z.string().optional()
|
|
1396
|
+
outputHint: import_zod.z.string().optional(),
|
|
1397
|
+
// Plan 050 — declarative pagination + response extraction.
|
|
1398
|
+
pagination: ApiPaginationSchema.optional(),
|
|
1399
|
+
response: ApiResponseMappingSchema.optional()
|
|
1273
1400
|
}).strict();
|
|
1274
1401
|
var HEADER_NAME_RE = /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/;
|
|
1275
1402
|
var RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
@@ -1278,6 +1405,14 @@ var RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
|
1278
1405
|
"set-cookie",
|
|
1279
1406
|
"proxy-authorization"
|
|
1280
1407
|
]);
|
|
1408
|
+
var SECRET_PATTERN_RE = /(-secret|-token|-key)$/i;
|
|
1409
|
+
var RESERVED_DEFAULT_HEADER_EXACT = /* @__PURE__ */ new Set([
|
|
1410
|
+
"authorization",
|
|
1411
|
+
"cookie",
|
|
1412
|
+
"set-cookie",
|
|
1413
|
+
"proxy-authorization",
|
|
1414
|
+
"x-auth-token"
|
|
1415
|
+
]);
|
|
1281
1416
|
var ApiServiceSchema = import_zod.z.object({
|
|
1282
1417
|
name: import_zod.z.string().min(1),
|
|
1283
1418
|
description: import_zod.z.string().optional(),
|
|
@@ -1292,6 +1427,34 @@ var ApiServiceSchema = import_zod.z.object({
|
|
|
1292
1427
|
endpoints: import_zod.z.array(ApiEndpointSchema).optional(),
|
|
1293
1428
|
secretHeaders: import_zod.z.record(import_zod.z.string(), import_zod.z.string().min(1)).optional()
|
|
1294
1429
|
}).strict().superRefine((svc, ctx) => {
|
|
1430
|
+
if (svc.defaultHeaders !== void 0) {
|
|
1431
|
+
for (const headerName of Object.keys(svc.defaultHeaders)) {
|
|
1432
|
+
if (!HEADER_NAME_RE.test(headerName)) {
|
|
1433
|
+
ctx.addIssue({
|
|
1434
|
+
code: "custom",
|
|
1435
|
+
message: `defaultHeaders key "${headerName}" is not a valid HTTP header name (RFC 7230 token charset)`,
|
|
1436
|
+
path: ["defaultHeaders", headerName]
|
|
1437
|
+
});
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
const lower = headerName.toLowerCase();
|
|
1441
|
+
if (RESERVED_DEFAULT_HEADER_EXACT.has(lower)) {
|
|
1442
|
+
ctx.addIssue({
|
|
1443
|
+
code: "custom",
|
|
1444
|
+
message: `defaultHeaders key "${headerName}" is reserved \u2014 move secret-bearing values to secretHeaders or primary auth`,
|
|
1445
|
+
path: ["defaultHeaders", headerName]
|
|
1446
|
+
});
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
if (SECRET_PATTERN_RE.test(headerName)) {
|
|
1450
|
+
ctx.addIssue({
|
|
1451
|
+
code: "custom",
|
|
1452
|
+
message: `defaultHeaders key "${headerName}" looks like a secret-bearing name (matches *-secret/*-token/*-key). Move to secretHeaders so the value is vault-resolved + scrubbed.`,
|
|
1453
|
+
path: ["defaultHeaders", headerName]
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1295
1458
|
if (svc.secretHeaders === void 0) return;
|
|
1296
1459
|
const lowerDefault = /* @__PURE__ */ new Set();
|
|
1297
1460
|
for (const k of Object.keys(svc.defaultHeaders ?? {})) lowerDefault.add(k.toLowerCase());
|
|
@@ -8124,6 +8287,11 @@ function getApiServicesSection(opts) {
|
|
|
8124
8287
|
"Configured external HTTP APIs. Use the `ApiCall` tool to invoke \u2014 auth is injected automatically. Do not pass credentials via headers."
|
|
8125
8288
|
);
|
|
8126
8289
|
lines.push("");
|
|
8290
|
+
appendStrictApiToolRules(
|
|
8291
|
+
lines,
|
|
8292
|
+
/* hasLazy */
|
|
8293
|
+
false
|
|
8294
|
+
);
|
|
8127
8295
|
for (const svc of withEndpoints) {
|
|
8128
8296
|
lines.push(`## ${svc.name}${svc.description ? ` \u2014 ${svc.description}` : ""}`);
|
|
8129
8297
|
lines.push(`baseUrl: ${svc.baseUrl}`);
|
|
@@ -8152,6 +8320,11 @@ function getApiServicesSection(opts) {
|
|
|
8152
8320
|
"Configured external HTTP APIs. Use `ApiCall` to invoke, but first call `DescribeService(service)` to fetch that service's endpoint catalog. Auth is injected automatically."
|
|
8153
8321
|
);
|
|
8154
8322
|
lines.push("");
|
|
8323
|
+
appendStrictApiToolRules(
|
|
8324
|
+
lines,
|
|
8325
|
+
/* hasLazy */
|
|
8326
|
+
true
|
|
8327
|
+
);
|
|
8155
8328
|
for (const svc of withEndpoints) {
|
|
8156
8329
|
const count = svc.endpoints.length;
|
|
8157
8330
|
const suffix = svc.description ? ` \u2014 ${svc.description}` : "";
|
|
@@ -8171,6 +8344,24 @@ function getApiServicesSection(opts) {
|
|
|
8171
8344
|
}
|
|
8172
8345
|
return lines.join("\n").trimEnd();
|
|
8173
8346
|
}
|
|
8347
|
+
function appendStrictApiToolRules(lines, hasLazy) {
|
|
8348
|
+
lines.push("Rules:");
|
|
8349
|
+
if (hasLazy) {
|
|
8350
|
+
lines.push(
|
|
8351
|
+
"- Call `DescribeService(name)` before `ApiCall` for any service shown above. The catalog gives you method, path, input schema, and (when declared) pagination + response-extraction metadata."
|
|
8352
|
+
);
|
|
8353
|
+
}
|
|
8354
|
+
lines.push(
|
|
8355
|
+
"- Auth headers (Authorization, X-API-Key, secret headers) are injected by the host. Never pass credentials via `ApiCall.headers`; the runtime drops them and your call may fail without them."
|
|
8356
|
+
);
|
|
8357
|
+
lines.push(
|
|
8358
|
+
"- For endpoints whose catalog entry declares `pagination`, prefer `ApiCall` with `pagination.auto: true` over manually issuing one call per page. The engine then handles request mutation, item extraction, and stop conditions deterministically."
|
|
8359
|
+
);
|
|
8360
|
+
lines.push(
|
|
8361
|
+
"- Tool results may be returned as opaque references when payloads are large. Inspect or otherwise read the shaped fields before deciding next steps. Never finalize using a raw reference id as data, and never include reference ids in user-visible output unless explicitly asked for debugging."
|
|
8362
|
+
);
|
|
8363
|
+
lines.push("");
|
|
8364
|
+
}
|
|
8174
8365
|
function resolveEffectiveMode(services, requested, threshold) {
|
|
8175
8366
|
if (requested === "eager" || requested === "lazy") return requested;
|
|
8176
8367
|
let total = 0;
|
|
@@ -8400,6 +8591,10 @@ init_contract();
|
|
|
8400
8591
|
var ALL_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
8401
8592
|
var DEFAULT_MAX_BODY_BYTES = 256 * 1024;
|
|
8402
8593
|
var DEFAULT_MAX_RESPONSE_BYTES = 100 * 1024;
|
|
8594
|
+
var DEFAULT_MAX_PAGES = 5;
|
|
8595
|
+
var MAX_PAGES_HARD_CAP = 50;
|
|
8596
|
+
var DEFAULT_MAX_ITEMS = 500;
|
|
8597
|
+
var MAX_ITEMS_HARD_CAP = 1e4;
|
|
8403
8598
|
function createApiCallTool(opts) {
|
|
8404
8599
|
if (opts.services.length === 0) {
|
|
8405
8600
|
throw new Error("createApiCallTool: services list must be non-empty");
|
|
@@ -8446,7 +8641,21 @@ function createApiCallTool(opts) {
|
|
|
8446
8641
|
* ingestion, image push, etc.).
|
|
8447
8642
|
*/
|
|
8448
8643
|
bodyEncoding: import_zod23.z.enum(["json", "raw"]).optional(),
|
|
8449
|
-
headers: import_zod23.z.record(import_zod23.z.string(), import_zod23.z.string()).optional()
|
|
8644
|
+
headers: import_zod23.z.record(import_zod23.z.string(), import_zod23.z.string()).optional(),
|
|
8645
|
+
/**
|
|
8646
|
+
* Plan 050 — opt-in auto-pagination. When `auto: true`, the
|
|
8647
|
+
* engine looks up the endpoint's pagination metadata from the
|
|
8648
|
+
* service catalog and loops pages deterministically. Without
|
|
8649
|
+
* `auto`, ApiCall behaves as a single-shot HTTP call (pre-050).
|
|
8650
|
+
*
|
|
8651
|
+
* `maxPages` defaults to 5, hard cap 50.
|
|
8652
|
+
* `maxItems` defaults to 500, hard cap 10000.
|
|
8653
|
+
*/
|
|
8654
|
+
pagination: import_zod23.z.object({
|
|
8655
|
+
auto: import_zod23.z.boolean().optional().default(false),
|
|
8656
|
+
maxPages: import_zod23.z.number().int().min(1).max(MAX_PAGES_HARD_CAP).optional(),
|
|
8657
|
+
maxItems: import_zod23.z.number().int().min(1).max(MAX_ITEMS_HARD_CAP).optional()
|
|
8658
|
+
}).optional()
|
|
8450
8659
|
});
|
|
8451
8660
|
const description = opts.toolDescription ?? `Call a configured external API. Services: ${serviceNames.join(", ")}. Auth is injected automatically \u2014 do not pass credentials via headers.`;
|
|
8452
8661
|
return defineTool({
|
|
@@ -8468,6 +8677,18 @@ function createApiCallTool(opts) {
|
|
|
8468
8677
|
if (!pathAllowed(input.path, effectivePaths)) {
|
|
8469
8678
|
return errResult(`ERR_API_PATH_NOT_ALLOWED: ${input.path} for service ${svc.name}`);
|
|
8470
8679
|
}
|
|
8680
|
+
if (input.pagination?.auto === true) {
|
|
8681
|
+
return executeAutoPaginated({
|
|
8682
|
+
svc,
|
|
8683
|
+
input,
|
|
8684
|
+
fetchFn,
|
|
8685
|
+
maxResponseBytes,
|
|
8686
|
+
env: opts.env,
|
|
8687
|
+
resolveAuth: opts.resolveAuth,
|
|
8688
|
+
onRequest: opts.onRequest,
|
|
8689
|
+
onResponse: opts.onResponse
|
|
8690
|
+
});
|
|
8691
|
+
}
|
|
8471
8692
|
let bodyText;
|
|
8472
8693
|
let defaultContentType;
|
|
8473
8694
|
if (input.body !== void 0) {
|
|
@@ -8666,6 +8887,287 @@ async function invokeHook(hook, event) {
|
|
|
8666
8887
|
} catch {
|
|
8667
8888
|
}
|
|
8668
8889
|
}
|
|
8890
|
+
function matchEndpoint(endpoints, method, path) {
|
|
8891
|
+
if (endpoints === void 0 || endpoints.length === 0) return { error: "NO_MATCH" };
|
|
8892
|
+
const exact = [];
|
|
8893
|
+
const templated = [];
|
|
8894
|
+
for (const ep of endpoints) {
|
|
8895
|
+
if (ep.method !== method) continue;
|
|
8896
|
+
if (ep.path === path) {
|
|
8897
|
+
exact.push(ep);
|
|
8898
|
+
continue;
|
|
8899
|
+
}
|
|
8900
|
+
if (ep.path.includes("{") && pathMatchesTemplate(ep.path, path)) {
|
|
8901
|
+
templated.push(ep);
|
|
8902
|
+
}
|
|
8903
|
+
}
|
|
8904
|
+
if (exact.length === 1) return { endpoint: exact[0] };
|
|
8905
|
+
if (exact.length > 1) return { error: "AMBIGUOUS" };
|
|
8906
|
+
if (templated.length === 1) return { endpoint: templated[0] };
|
|
8907
|
+
if (templated.length > 1) return { error: "AMBIGUOUS" };
|
|
8908
|
+
return { error: "NO_MATCH" };
|
|
8909
|
+
}
|
|
8910
|
+
function pathMatchesTemplate(template, concrete) {
|
|
8911
|
+
const pattern = "^" + template.split(/(\{[^}]+\})/).map(
|
|
8912
|
+
(seg) => seg.startsWith("{") && seg.endsWith("}") ? "[^/]+" : seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
8913
|
+
).join("") + "$";
|
|
8914
|
+
return new RegExp(pattern).test(concrete);
|
|
8915
|
+
}
|
|
8916
|
+
function extractByDotPath(value, path) {
|
|
8917
|
+
if (path.length === 0) return value;
|
|
8918
|
+
let cur = value;
|
|
8919
|
+
for (const seg of path.split(".")) {
|
|
8920
|
+
if (cur === null || typeof cur !== "object") return void 0;
|
|
8921
|
+
cur = cur[seg];
|
|
8922
|
+
}
|
|
8923
|
+
return cur;
|
|
8924
|
+
}
|
|
8925
|
+
function asItemsArray(value) {
|
|
8926
|
+
return Array.isArray(value) ? value : null;
|
|
8927
|
+
}
|
|
8928
|
+
function asTotalCount(value) {
|
|
8929
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
8930
|
+
return Math.floor(value);
|
|
8931
|
+
}
|
|
8932
|
+
if (typeof value === "string") {
|
|
8933
|
+
const n = Number(value);
|
|
8934
|
+
if (Number.isFinite(n) && n >= 0) return Math.floor(n);
|
|
8935
|
+
}
|
|
8936
|
+
return null;
|
|
8937
|
+
}
|
|
8938
|
+
async function executeAutoPaginated(args) {
|
|
8939
|
+
const { svc, input, fetchFn, maxResponseBytes } = args;
|
|
8940
|
+
const matched = matchEndpoint(svc.endpoints, input.method, input.path);
|
|
8941
|
+
if ("error" in matched) {
|
|
8942
|
+
if (matched.error === "NO_MATCH") {
|
|
8943
|
+
return errResult(
|
|
8944
|
+
`ERR_API_PAGINATION_NO_METADATA: no catalog entry for ${input.method} ${input.path} on service ${svc.name} \u2014 auto-pagination requires endpoints[].pagination`
|
|
8945
|
+
);
|
|
8946
|
+
}
|
|
8947
|
+
return errResult(
|
|
8948
|
+
`ERR_API_ENDPOINT_AMBIGUOUS: ${input.method} ${input.path} matched multiple catalog entries on service ${svc.name}`
|
|
8949
|
+
);
|
|
8950
|
+
}
|
|
8951
|
+
const endpoint = matched.endpoint;
|
|
8952
|
+
const p = endpoint.pagination;
|
|
8953
|
+
if (p === void 0) {
|
|
8954
|
+
return errResult(
|
|
8955
|
+
`ERR_API_PAGINATION_NO_METADATA: endpoint ${input.method} ${input.path} on service ${svc.name} has no pagination metadata; remove pagination.auto or declare it in the catalog`
|
|
8956
|
+
);
|
|
8957
|
+
}
|
|
8958
|
+
const itemsPath = p.response?.itemsPath ?? endpoint.response?.itemsPath;
|
|
8959
|
+
if (itemsPath === void 0) {
|
|
8960
|
+
return errResult(
|
|
8961
|
+
`ERR_API_PAGINATION_NO_METADATA: endpoint ${input.method} ${input.path} declares pagination but no response.itemsPath; cannot extract items`
|
|
8962
|
+
);
|
|
8963
|
+
}
|
|
8964
|
+
const maxPages = Math.min(input.pagination?.maxPages ?? DEFAULT_MAX_PAGES, MAX_PAGES_HARD_CAP);
|
|
8965
|
+
const maxItems = Math.min(input.pagination?.maxItems ?? DEFAULT_MAX_ITEMS, MAX_ITEMS_HARD_CAP);
|
|
8966
|
+
const secretHeaderRefs = svc.secretHeaders !== void 0 && Object.keys(svc.secretHeaders).length > 0 ? svc.secretHeaders : void 0;
|
|
8967
|
+
let authHeaders;
|
|
8968
|
+
try {
|
|
8969
|
+
authHeaders = await resolveAuth({
|
|
8970
|
+
auth: svc.auth ?? { type: "none" },
|
|
8971
|
+
env: args.env,
|
|
8972
|
+
resolver: args.resolveAuth,
|
|
8973
|
+
ctx: {
|
|
8974
|
+
serviceName: svc.name,
|
|
8975
|
+
method: input.method,
|
|
8976
|
+
path: input.path,
|
|
8977
|
+
...secretHeaderRefs !== void 0 ? { secretHeaderRefs } : {}
|
|
8978
|
+
},
|
|
8979
|
+
forceResolve: secretHeaderRefs !== void 0
|
|
8980
|
+
});
|
|
8981
|
+
} catch (err) {
|
|
8982
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
8983
|
+
const truncated = raw.length > 200 ? raw.slice(0, 200) + "\u2026" : raw;
|
|
8984
|
+
return errResult(`ERR_API_RESOLVER_FAILED: ${truncated}`);
|
|
8985
|
+
}
|
|
8986
|
+
const userHeaders = sanitizeHeaders(input.headers ?? {}, authHeaders);
|
|
8987
|
+
const aggregated = [];
|
|
8988
|
+
let pagesFetched = 0;
|
|
8989
|
+
let stoppedBy = "unknown";
|
|
8990
|
+
let lastStatus = 0;
|
|
8991
|
+
let nextOffset = 0;
|
|
8992
|
+
let nextPage = p.request?.firstPage ?? 1;
|
|
8993
|
+
let nextCursor = null;
|
|
8994
|
+
let nextLink = null;
|
|
8995
|
+
while (pagesFetched < maxPages && aggregated.length < maxItems) {
|
|
8996
|
+
const pageQuery = { ...input.query ?? {} };
|
|
8997
|
+
let pageUrl = null;
|
|
8998
|
+
if (p.mode === "offset") {
|
|
8999
|
+
const limit = p.request?.maxLimit ?? 100;
|
|
9000
|
+
if (p.request?.limitParam) pageQuery[p.request.limitParam] = String(limit);
|
|
9001
|
+
if (p.request?.offsetParam) pageQuery[p.request.offsetParam] = String(nextOffset);
|
|
9002
|
+
} else if (p.mode === "page") {
|
|
9003
|
+
if (p.request?.maxLimit !== void 0 && p.request?.limitParam) {
|
|
9004
|
+
pageQuery[p.request.limitParam] = String(p.request.maxLimit);
|
|
9005
|
+
}
|
|
9006
|
+
if (p.request?.pageParam) pageQuery[p.request.pageParam] = String(nextPage);
|
|
9007
|
+
} else if (p.mode === "cursor") {
|
|
9008
|
+
if (p.request?.maxLimit !== void 0 && p.request?.limitParam) {
|
|
9009
|
+
pageQuery[p.request.limitParam] = String(p.request.maxLimit);
|
|
9010
|
+
}
|
|
9011
|
+
if (nextCursor !== null && p.request?.cursorParam) {
|
|
9012
|
+
pageQuery[p.request.cursorParam] = nextCursor;
|
|
9013
|
+
}
|
|
9014
|
+
} else if (p.mode === "link-header") {
|
|
9015
|
+
if (nextLink !== null) pageUrl = nextLink;
|
|
9016
|
+
}
|
|
9017
|
+
const url = pageUrl ?? buildUrl(svc.baseUrl, input.path, pageQuery);
|
|
9018
|
+
await invokeHook(args.onRequest, {
|
|
9019
|
+
service: svc.name,
|
|
9020
|
+
method: input.method,
|
|
9021
|
+
path: input.path
|
|
9022
|
+
});
|
|
9023
|
+
const started = Date.now();
|
|
9024
|
+
let res;
|
|
9025
|
+
try {
|
|
9026
|
+
res = await fetchFn(url, {
|
|
9027
|
+
method: input.method,
|
|
9028
|
+
headers: {
|
|
9029
|
+
...svc.defaultHeaders ?? {},
|
|
9030
|
+
...userHeaders,
|
|
9031
|
+
...authHeaders
|
|
9032
|
+
}
|
|
9033
|
+
});
|
|
9034
|
+
} catch (err) {
|
|
9035
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9036
|
+
return paginationErr({
|
|
9037
|
+
code: "ERR_API_PAGINATION_PAGE_FAILED",
|
|
9038
|
+
message: `${input.method} ${input.path} page ${pagesFetched + 1} network error: ${msg}`,
|
|
9039
|
+
pagesFetched,
|
|
9040
|
+
itemsFetched: aggregated.length,
|
|
9041
|
+
partialItems: aggregated
|
|
9042
|
+
});
|
|
9043
|
+
}
|
|
9044
|
+
lastStatus = res.status;
|
|
9045
|
+
const raw = await res.text();
|
|
9046
|
+
const captured = raw.length > maxResponseBytes ? raw.slice(0, maxResponseBytes) + "\n\u2026[TRUNCATED]" : raw;
|
|
9047
|
+
await invokeHook(args.onResponse, {
|
|
9048
|
+
service: svc.name,
|
|
9049
|
+
method: input.method,
|
|
9050
|
+
path: input.path,
|
|
9051
|
+
status: res.status,
|
|
9052
|
+
latencyMs: Date.now() - started,
|
|
9053
|
+
bytesIn: raw.length
|
|
9054
|
+
});
|
|
9055
|
+
if (!res.ok) {
|
|
9056
|
+
return paginationErr({
|
|
9057
|
+
code: "ERR_API_PAGINATION_PAGE_FAILED",
|
|
9058
|
+
message: `${input.method} ${input.path} page ${pagesFetched + 1} returned status ${String(res.status)}`,
|
|
9059
|
+
pagesFetched,
|
|
9060
|
+
itemsFetched: aggregated.length,
|
|
9061
|
+
partialItems: aggregated,
|
|
9062
|
+
responsePreview: captured
|
|
9063
|
+
});
|
|
9064
|
+
}
|
|
9065
|
+
let body;
|
|
9066
|
+
try {
|
|
9067
|
+
body = JSON.parse(captured);
|
|
9068
|
+
} catch (err) {
|
|
9069
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9070
|
+
return paginationErr({
|
|
9071
|
+
code: "ERR_API_PAGINATION_PAGE_FAILED",
|
|
9072
|
+
message: `${input.method} ${input.path} page ${pagesFetched + 1} returned non-JSON: ${msg}`,
|
|
9073
|
+
pagesFetched,
|
|
9074
|
+
itemsFetched: aggregated.length,
|
|
9075
|
+
partialItems: aggregated
|
|
9076
|
+
});
|
|
9077
|
+
}
|
|
9078
|
+
const itemsRaw = extractByDotPath(body, itemsPath);
|
|
9079
|
+
const items = asItemsArray(itemsRaw);
|
|
9080
|
+
if (items === null) {
|
|
9081
|
+
return errResult(
|
|
9082
|
+
`ERR_API_ITEMS_PATH_INVALID: ${input.method} ${input.path} response.${itemsPath} did not resolve to an array`
|
|
9083
|
+
);
|
|
9084
|
+
}
|
|
9085
|
+
pagesFetched += 1;
|
|
9086
|
+
for (const it of items) {
|
|
9087
|
+
if (aggregated.length >= maxItems) break;
|
|
9088
|
+
aggregated.push(it);
|
|
9089
|
+
}
|
|
9090
|
+
const stop = p.stop?.strategy;
|
|
9091
|
+
if (stop === "empty-page" && items.length === 0) {
|
|
9092
|
+
stoppedBy = "empty-page";
|
|
9093
|
+
break;
|
|
9094
|
+
}
|
|
9095
|
+
if (stop === "total-reached" && p.response?.totalPath !== void 0) {
|
|
9096
|
+
const total = asTotalCount(extractByDotPath(body, p.response.totalPath));
|
|
9097
|
+
if (total !== null && aggregated.length >= total) {
|
|
9098
|
+
stoppedBy = "total-reached";
|
|
9099
|
+
break;
|
|
9100
|
+
}
|
|
9101
|
+
}
|
|
9102
|
+
if (p.mode === "cursor") {
|
|
9103
|
+
const cursorRaw = p.response?.nextCursorPath ? extractByDotPath(body, p.response.nextCursorPath) : void 0;
|
|
9104
|
+
const cursor = typeof cursorRaw === "string" && cursorRaw.length > 0 ? cursorRaw : null;
|
|
9105
|
+
if (cursor === null) {
|
|
9106
|
+
stoppedBy = "missing-cursor";
|
|
9107
|
+
break;
|
|
9108
|
+
}
|
|
9109
|
+
nextCursor = cursor;
|
|
9110
|
+
} else if (p.mode === "link-header") {
|
|
9111
|
+
const linkRaw = p.response?.nextLinkPath ? extractByDotPath(body, p.response.nextLinkPath) : void 0;
|
|
9112
|
+
const link = typeof linkRaw === "string" && linkRaw.length > 0 ? linkRaw : null;
|
|
9113
|
+
if (link === null) {
|
|
9114
|
+
stoppedBy = "missing-next-link";
|
|
9115
|
+
break;
|
|
9116
|
+
}
|
|
9117
|
+
nextLink = link;
|
|
9118
|
+
} else if (p.mode === "offset") {
|
|
9119
|
+
if (items.length === 0) {
|
|
9120
|
+
stoppedBy = "empty-page-fallback";
|
|
9121
|
+
break;
|
|
9122
|
+
}
|
|
9123
|
+
nextOffset += items.length;
|
|
9124
|
+
} else if (p.mode === "page") {
|
|
9125
|
+
if (items.length === 0) {
|
|
9126
|
+
stoppedBy = "empty-page-fallback";
|
|
9127
|
+
break;
|
|
9128
|
+
}
|
|
9129
|
+
nextPage += 1;
|
|
9130
|
+
}
|
|
9131
|
+
}
|
|
9132
|
+
if (stoppedBy === "unknown") {
|
|
9133
|
+
if (aggregated.length >= maxItems) stoppedBy = "max-items";
|
|
9134
|
+
else if (pagesFetched >= maxPages) stoppedBy = "max-pages";
|
|
9135
|
+
}
|
|
9136
|
+
const payload = {
|
|
9137
|
+
ok: true,
|
|
9138
|
+
status: lastStatus || 200,
|
|
9139
|
+
items: aggregated,
|
|
9140
|
+
pagination: {
|
|
9141
|
+
mode: p.mode,
|
|
9142
|
+
pagesFetched,
|
|
9143
|
+
itemsFetched: aggregated.length,
|
|
9144
|
+
stoppedBy
|
|
9145
|
+
}
|
|
9146
|
+
};
|
|
9147
|
+
return {
|
|
9148
|
+
content: JSON.stringify(payload),
|
|
9149
|
+
metadata: {
|
|
9150
|
+
status: lastStatus || 200,
|
|
9151
|
+
service: svc.name,
|
|
9152
|
+
pagesFetched,
|
|
9153
|
+
itemsFetched: aggregated.length,
|
|
9154
|
+
stoppedBy
|
|
9155
|
+
}
|
|
9156
|
+
};
|
|
9157
|
+
}
|
|
9158
|
+
function paginationErr(args) {
|
|
9159
|
+
const payload = {
|
|
9160
|
+
ok: false,
|
|
9161
|
+
error: { code: args.code, message: args.message },
|
|
9162
|
+
pagination: {
|
|
9163
|
+
pagesFetched: args.pagesFetched,
|
|
9164
|
+
itemsFetched: args.itemsFetched
|
|
9165
|
+
},
|
|
9166
|
+
partialItems: args.partialItems,
|
|
9167
|
+
...args.responsePreview !== void 0 ? { responsePreview: args.responsePreview.slice(0, 4096) } : {}
|
|
9168
|
+
};
|
|
9169
|
+
return { content: JSON.stringify(payload), isError: true };
|
|
9170
|
+
}
|
|
8669
9171
|
|
|
8670
9172
|
// src/tools/describeService.ts
|
|
8671
9173
|
init_cjs_shims();
|
|
@@ -8678,7 +9180,12 @@ function describe(svc) {
|
|
|
8678
9180
|
path: ep.path,
|
|
8679
9181
|
description: ep.description,
|
|
8680
9182
|
...ep.inputSchema !== void 0 ? { inputSchema: ep.inputSchema } : {},
|
|
8681
|
-
...ep.outputHint !== void 0 ? { outputHint: ep.outputHint } : {}
|
|
9183
|
+
...ep.outputHint !== void 0 ? { outputHint: ep.outputHint } : {},
|
|
9184
|
+
// Plan 050 — surface pagination + response extraction metadata
|
|
9185
|
+
// so the model knows it can request `pagination.auto: true` and
|
|
9186
|
+
// sees the declared items / total / cursor paths inline.
|
|
9187
|
+
...ep.pagination !== void 0 ? { pagination: ep.pagination } : {},
|
|
9188
|
+
...ep.response !== void 0 ? { response: ep.response } : {}
|
|
8682
9189
|
})
|
|
8683
9190
|
);
|
|
8684
9191
|
return {
|
|
@@ -8705,7 +9212,7 @@ function createDescribeServiceTool(opts) {
|
|
|
8705
9212
|
const inputSchema19 = import_zod24.z.object({
|
|
8706
9213
|
service: import_zod24.z.enum(serviceNames)
|
|
8707
9214
|
});
|
|
8708
|
-
const description = `Look up the endpoint catalog for one configured API service. Returns every endpoint's method, path, description, and
|
|
9215
|
+
const description = `Look up the endpoint catalog for one configured API service. Returns every endpoint's method, path, description, input schema, and (when declared) pagination + response-extraction metadata. Call this before invoking \`ApiCall\` when the service has lazy endpoints; for paginated list endpoints, prefer \`ApiCall\` with \`pagination.auto: true\` over manually issuing one call per page. Services: ${serviceNames.join(", ")}.`;
|
|
8709
9216
|
return defineTool({
|
|
8710
9217
|
name: opts.toolName ?? "DescribeService",
|
|
8711
9218
|
description,
|