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 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 input schema. Call this before invoking \`ApiCall\` when the service has lazy endpoints. Services: ${serviceNames.join(", ")}.`;
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,