la-machina-engine 0.15.1 → 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 +475 -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 +475 -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([
|
|
@@ -8160,6 +8287,11 @@ function getApiServicesSection(opts) {
|
|
|
8160
8287
|
"Configured external HTTP APIs. Use the `ApiCall` tool to invoke \u2014 auth is injected automatically. Do not pass credentials via headers."
|
|
8161
8288
|
);
|
|
8162
8289
|
lines.push("");
|
|
8290
|
+
appendStrictApiToolRules(
|
|
8291
|
+
lines,
|
|
8292
|
+
/* hasLazy */
|
|
8293
|
+
false
|
|
8294
|
+
);
|
|
8163
8295
|
for (const svc of withEndpoints) {
|
|
8164
8296
|
lines.push(`## ${svc.name}${svc.description ? ` \u2014 ${svc.description}` : ""}`);
|
|
8165
8297
|
lines.push(`baseUrl: ${svc.baseUrl}`);
|
|
@@ -8188,6 +8320,11 @@ function getApiServicesSection(opts) {
|
|
|
8188
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."
|
|
8189
8321
|
);
|
|
8190
8322
|
lines.push("");
|
|
8323
|
+
appendStrictApiToolRules(
|
|
8324
|
+
lines,
|
|
8325
|
+
/* hasLazy */
|
|
8326
|
+
true
|
|
8327
|
+
);
|
|
8191
8328
|
for (const svc of withEndpoints) {
|
|
8192
8329
|
const count = svc.endpoints.length;
|
|
8193
8330
|
const suffix = svc.description ? ` \u2014 ${svc.description}` : "";
|
|
@@ -8207,6 +8344,24 @@ function getApiServicesSection(opts) {
|
|
|
8207
8344
|
}
|
|
8208
8345
|
return lines.join("\n").trimEnd();
|
|
8209
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
|
+
}
|
|
8210
8365
|
function resolveEffectiveMode(services, requested, threshold) {
|
|
8211
8366
|
if (requested === "eager" || requested === "lazy") return requested;
|
|
8212
8367
|
let total = 0;
|
|
@@ -8436,6 +8591,10 @@ init_contract();
|
|
|
8436
8591
|
var ALL_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
8437
8592
|
var DEFAULT_MAX_BODY_BYTES = 256 * 1024;
|
|
8438
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;
|
|
8439
8598
|
function createApiCallTool(opts) {
|
|
8440
8599
|
if (opts.services.length === 0) {
|
|
8441
8600
|
throw new Error("createApiCallTool: services list must be non-empty");
|
|
@@ -8482,7 +8641,21 @@ function createApiCallTool(opts) {
|
|
|
8482
8641
|
* ingestion, image push, etc.).
|
|
8483
8642
|
*/
|
|
8484
8643
|
bodyEncoding: import_zod23.z.enum(["json", "raw"]).optional(),
|
|
8485
|
-
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()
|
|
8486
8659
|
});
|
|
8487
8660
|
const description = opts.toolDescription ?? `Call a configured external API. Services: ${serviceNames.join(", ")}. Auth is injected automatically \u2014 do not pass credentials via headers.`;
|
|
8488
8661
|
return defineTool({
|
|
@@ -8504,6 +8677,18 @@ function createApiCallTool(opts) {
|
|
|
8504
8677
|
if (!pathAllowed(input.path, effectivePaths)) {
|
|
8505
8678
|
return errResult(`ERR_API_PATH_NOT_ALLOWED: ${input.path} for service ${svc.name}`);
|
|
8506
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
|
+
}
|
|
8507
8692
|
let bodyText;
|
|
8508
8693
|
let defaultContentType;
|
|
8509
8694
|
if (input.body !== void 0) {
|
|
@@ -8702,6 +8887,287 @@ async function invokeHook(hook, event) {
|
|
|
8702
8887
|
} catch {
|
|
8703
8888
|
}
|
|
8704
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
|
+
}
|
|
8705
9171
|
|
|
8706
9172
|
// src/tools/describeService.ts
|
|
8707
9173
|
init_cjs_shims();
|
|
@@ -8714,7 +9180,12 @@ function describe(svc) {
|
|
|
8714
9180
|
path: ep.path,
|
|
8715
9181
|
description: ep.description,
|
|
8716
9182
|
...ep.inputSchema !== void 0 ? { inputSchema: ep.inputSchema } : {},
|
|
8717
|
-
...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 } : {}
|
|
8718
9189
|
})
|
|
8719
9190
|
);
|
|
8720
9191
|
return {
|
|
@@ -8741,7 +9212,7 @@ function createDescribeServiceTool(opts) {
|
|
|
8741
9212
|
const inputSchema19 = import_zod24.z.object({
|
|
8742
9213
|
service: import_zod24.z.enum(serviceNames)
|
|
8743
9214
|
});
|
|
8744
|
-
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(", ")}.`;
|
|
8745
9216
|
return defineTool({
|
|
8746
9217
|
name: opts.toolName ?? "DescribeService",
|
|
8747
9218
|
description,
|