@pipelex/sdk 0.15.0 → 0.17.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/client.js CHANGED
@@ -6,7 +6,7 @@ import { pollUntilResult, } from "./runs.js";
6
6
  import { ApiResponseError, ApiUnreachableError, EmptyMethodSourceError, MissingMainStuffError, PipelineExecuteTimeoutError, PipelineRequestError, RunLifecycleUnavailableError, RunStillRunningError, } from "./errors.js";
7
7
  import { methodSourceToContents } from "./method-source.js";
8
8
  import { uploadFile as uploadFileImpl } from "./upload.js";
9
- import { prepareInputs as prepareInputsImpl, resolveFilesOrMethodId } from "./prepare-inputs.js";
9
+ import { prepareInputs as prepareInputsImpl } from "./prepare-inputs.js";
10
10
  import { PipelexExecuteResult } from "./execute-result.js";
11
11
  // A pure RUNAWAY guard on `iterateMethods`, deliberately not a coverage limit.
12
12
  //
@@ -266,15 +266,18 @@ export class PipelexApiClient {
266
266
  */
267
267
  async execute(options) {
268
268
  const extensions = buildExtensions(options.extra);
269
+ const api = buildApiRunExtensions(options);
269
270
  const hosted = buildHostedRunExtensions(options);
270
271
  if (!options.pipe_code &&
271
272
  (!options.mthds_contents || options.mthds_contents.length === 0) &&
272
273
  !hasBundlePayload(options) &&
274
+ Object.keys(api).length === 0 &&
273
275
  Object.keys(hosted).length === 0 &&
274
276
  Object.keys(extensions).length === 0) {
275
- throw new PipelineRequestError("Either pipe_code, mthds_contents, a method bundle (files/bundle_b64), a hosted method_id or a server-specific extension arg (extra) must be provided to execute().");
277
+ throw new PipelineRequestError("Either pipe_code, mthds_contents, a method bundle (files/bundle_b64), a method_ref, a hosted method_id or a server-specific extension arg (extra) must be provided to execute().");
276
278
  }
277
279
  assertExclusiveRunSources(options);
280
+ assertMethodRefPairsWithNothing(options);
278
281
  const request = {
279
282
  pipe_code: options.pipe_code,
280
283
  mthds_contents: options.mthds_contents,
@@ -284,6 +287,7 @@ export class PipelexApiClient {
284
287
  dynamic_output_concept_ref: options.dynamic_output_concept_ref,
285
288
  files: nonEmptyFiles(options.files),
286
289
  bundle_b64: nonEmptyString(options.bundle_b64),
290
+ ...api,
287
291
  ...hosted,
288
292
  ...extensions,
289
293
  };
@@ -316,24 +320,30 @@ export class PipelexApiClient {
316
320
  /**
317
321
  * Start a method asynchronously — `POST /v1/start` (202, no output yet).
318
322
  *
319
- * The hosted `method_id` is a named option (see `PipelexHostedRunExtensions`);
320
- * `options.extra` stays the generic passthrough for extension args this client
321
- * does not know about — the server you call defines and handles them
322
- * (including a client-supplied run id where a server supports one). The
323
- * returned `pipeline_run_id` is always authoritative; on a hosted deployment
324
- * it is durable — poll `getRunStatus` / `getRunResult`.
323
+ * The `method_ref` address and the hosted `method_id` are named options (see
324
+ * `PipelexApiRunExtensions` / `PipelexHostedRunExtensions`); `options.extra`
325
+ * stays the generic passthrough for extension args this client does not know
326
+ * about — the server you call defines and handles them (including a
327
+ * client-supplied run id where a server supports one). The returned
328
+ * `pipeline_run_id` is always authoritative; on a hosted deployment it is
329
+ * durable — poll `getRunStatus` / `getRunResult`. A `method_ref` run's ack
330
+ * additionally carries `method_provenance` — the address, the tag, and the
331
+ * commit SHA that was actually fetched.
325
332
  */
326
333
  async start(options) {
327
334
  const extensions = buildExtensions(options.extra);
335
+ const api = buildApiRunExtensions(options);
328
336
  const hosted = buildHostedRunExtensions(options);
329
337
  if (!options.pipe_code &&
330
338
  (!options.mthds_contents || options.mthds_contents.length === 0) &&
331
339
  !hasBundlePayload(options) &&
340
+ Object.keys(api).length === 0 &&
332
341
  Object.keys(hosted).length === 0 &&
333
342
  Object.keys(extensions).length === 0) {
334
- throw new PipelineRequestError("Either pipe_code, mthds_contents, a method bundle (files/bundle_b64), a hosted method_id or a server-specific extension arg (extra) must be provided to start().");
343
+ throw new PipelineRequestError("Either pipe_code, mthds_contents, a method bundle (files/bundle_b64), a method_ref, a hosted method_id or a server-specific extension arg (extra) must be provided to start().");
335
344
  }
336
345
  assertExclusiveRunSources(options);
346
+ assertMethodRefPairsWithNothing(options);
337
347
  // `?? undefined` so JSON.stringify drops absent fields from the wire body.
338
348
  const request = {
339
349
  pipe_code: options.pipe_code ?? undefined,
@@ -344,18 +354,24 @@ export class PipelexApiClient {
344
354
  dynamic_output_concept_ref: options.dynamic_output_concept_ref ?? undefined,
345
355
  files: nonEmptyFiles(options.files),
346
356
  bundle_b64: nonEmptyString(options.bundle_b64),
357
+ ...api,
347
358
  ...hosted,
348
359
  ...extensions,
349
360
  };
350
361
  const url = this.url("start");
351
- // `start` returns a 202 fast, so the poll timeout normally fits — but a
352
- // method bundle can make the request *body* multi-megabyte, and the whole
353
- // upload is charged against this budget. Give a bundle-carrying start the
354
- // same upload ceiling the blocking `execute` path has, so the same payload
355
- // can't time out on the durable path yet succeed on the fallback.
362
+ // `start` returns a 202 fast, so the poll timeout normally fits — with two
363
+ // exceptions that get the blocking-execute ceiling instead. A method bundle
364
+ // can make the request *body* multi-megabyte, and the whole upload is
365
+ // charged against this budget — the same payload must not time out on the
366
+ // durable path yet succeed on the fallback. And a `method_ref` start makes
367
+ // the server FETCH the package before the ack (provenance rides the 202),
368
+ // whose clone timeout runs well past 30s on a cold cache — aborting it here
369
+ // would surface as `ApiUnreachableError`, blaming the network for a healthy
370
+ // server that is still cloning.
371
+ const needsLongCeiling = hasBundlePayload(options) || nonEmptyString(options.method_ref) !== undefined;
356
372
  const res = await this.requestRaw("POST", url, {
357
373
  body: request,
358
- timeoutMs: hasBundlePayload(options) ? DEFAULT_REQUEST_TIMEOUT_MS : POLL_REQUEST_TIMEOUT_MS,
374
+ timeoutMs: needsLongCeiling ? DEFAULT_REQUEST_TIMEOUT_MS : POLL_REQUEST_TIMEOUT_MS,
359
375
  });
360
376
  // A bare runner with no run store 404s here just as it does on the result
361
377
  // routes — surface the same clear `RunLifecycleUnavailableError` (and let
@@ -377,11 +393,32 @@ export class PipelexApiClient {
377
393
  * *no-verdict* condition (a malformed request, an `mthds_sources` length mismatch,
378
394
  * auth, a server fault) is non-2xx and surfaces as `ApiResponseError`.
379
395
  *
380
- * `mthdsSources` (optional, parallel to `mthdsContents`) names each submitted
396
+ * `source` selects WHAT is validated, in exactly one of three forms — the
397
+ * strict tooling XOR (the routes are stateless, so there is no linkage
398
+ * exception; a second selector is a request-shape `422`):
399
+ *
400
+ * - **inline contents** (a `string[]`) — the protocol's own envelope;
401
+ * - **`{ method_ref }`** — a published method's address, resolved by the
402
+ * server (pipelex-api >= 0.21.0) through the same fetch path as a
403
+ * `method_ref` run, the package's real file names feeding the diagnostics'
404
+ * source labels;
405
+ * - **`{ method_id }`** — a stored method's catalog id, hosted-only: the
406
+ * platform resolves it and injects the stored source before the runner sees
407
+ * the request (a bare runner rejects the request as carrying no source it
408
+ * understands).
409
+ *
410
+ * A selector-resolution failure (fetch failure, no package at the address, an
411
+ * unknown or foreign-org id) is a non-2xx `ApiResponseError` — never an
412
+ * `is_valid: false` verdict, which is reserved for actual MTHDS content.
413
+ *
414
+ * `mthdsSources` (optional, parallel to inline contents) names each submitted
381
415
  * content — a Pipelex-API extension threaded onto `blueprint.source`, so
382
416
  * cross-file diagnostics name the owning file (an unnamed content yields
383
417
  * `source: null`). The server 422s a length mismatch; this client sends the
384
- * arrays verbatim and surfaces that as an `ApiResponseError`.
418
+ * arrays verbatim and surfaces that as an `ApiResponseError`. It is an
419
+ * inline-contents companion only: a `method_ref` / `method_id` validation gets
420
+ * its source labels from the package's (or the stored method's) real file
421
+ * names, so supplying it beside a selector is rejected client-side.
385
422
  *
386
423
  * `render` is the Pipelex-API presentation hint — a list of view-format tokens.
387
424
  * This client always asks for Markdown so both valid results and produced
@@ -391,7 +428,7 @@ export class PipelexApiClient {
391
428
  * `views` is the sibling opt-in for *structured* views where `render` carries
392
429
  * *rendered text*. The two lists are independent, each resolving its own tokens
393
430
  * against its own supported set, and each supported token adds a same-named
394
- * top-level field to the valid arm — today only `input_form`. Unlike `render`,
431
+ * top-level field to the valid arm — `input_form` and `output_form`. Unlike `render`,
395
432
  * this client sends `views` ONLY when the caller asks: the point of an opt-in view
396
433
  * is that the default response stays byte-identical, and the highest-frequency
397
434
  * consumers (hook pipelines, CI gates, agent loops) never pay for bytes they
@@ -399,14 +436,49 @@ export class PipelexApiClient {
399
436
  * runner resolved no token (the key is silently ignored, never a 422) and emitted
400
437
  * the field regardless — which is why `PipelexValidationReport.input_form` is typed
401
438
  * optional rather than required.
439
+ *
440
+ * A `{ method_ref }` source makes the server clone a repository before it
441
+ * validates anything, and it needs no special budget here: this route already
442
+ * defaults to the 20-minute execute ceiling, which clears the internal
443
+ * fetch-sized budget (`METHOD_REF_FETCH_TIMEOUT_MS`, three minutes) that
444
+ * `crateRequestTimeoutMs` gives the crate routes several times over. That budget
445
+ * exists to RAISE routes whose default is the ~30s poll ceiling; applying it here
446
+ * would lower this one — which is why `buildRunner`, on its own five-minute
447
+ * default, is excluded from it for the same reason.
402
448
  */
403
- async validate(mthdsContents, allowSignatures = false, mthdsSources, render, views, options = {}) {
449
+ async validate(source, allowSignatures = false, mthdsSources, render, views, options = {}) {
404
450
  const body = {
405
- mthds_contents: mthdsContents,
406
451
  allow_signatures: allowSignatures,
407
452
  };
408
- if (mthdsSources !== undefined) {
409
- body.mthds_sources = mthdsSources;
453
+ if (Array.isArray(source)) {
454
+ body.mthds_contents = source;
455
+ if (mthdsSources !== undefined) {
456
+ body.mthds_sources = mthdsSources;
457
+ }
458
+ }
459
+ else {
460
+ // A selector object. The illegal shapes are compile errors for typed
461
+ // callers (`ValidateMethodSelector` pins the other key to `never`); the
462
+ // runtime checks back them for untyped (JS) callers — a typed
463
+ // `PipelineRequestError`, never a native TypeError off a null source —
464
+ // mirroring the server's strict tooling XOR instead of silently picking.
465
+ if (source === null || source === undefined || typeof source !== "object") {
466
+ throw new PipelineRequestError("validate() takes inline contents (a string[]) or a method selector object " +
467
+ "({ method_ref } or { method_id }).");
468
+ }
469
+ const methodRef = nonEmptyString(source.method_ref);
470
+ const methodId = nonEmptyString(source.method_id);
471
+ if ((methodRef === undefined) === (methodId === undefined)) {
472
+ throw new PipelineRequestError("validate() takes exactly one method selector: inline contents, { method_ref }, or { method_id }.");
473
+ }
474
+ if (mthdsSources !== undefined) {
475
+ throw new PipelineRequestError("mthds_sources labels inline mthds_contents; a method_ref / method_id validation gets " +
476
+ "its source labels from the package's (or the stored method's) real file names.");
477
+ }
478
+ if (methodRef !== undefined)
479
+ body.method_ref = methodRef;
480
+ if (methodId !== undefined)
481
+ body.method_id = methodId;
410
482
  }
411
483
  body.render = withValidateMarkdownRender(render);
412
484
  if (views !== undefined) {
@@ -510,7 +582,8 @@ export class PipelexApiClient {
510
582
  * The mapping is what makes their no-verdict arms usable: a crate-family route
511
583
  * answers `422` for a request it cannot act on (an unresolvable pipe selector on the
512
584
  * build routes; an unknown `kind`/`target`, or a `pipe_ref` on the concept-set-wide
513
- * `types` kind, on `codegen`) and `501` for the reserved `method_ref`. A caller
585
+ * `types` kind, on `codegen`) and `501` for the reserved registry-form `method_ref`
586
+ * (the address form is resolved server-side as of pipelex-api 0.21.0). A caller
514
587
  * branches on `ApiResponseError.status`, never on a message.
515
588
  *
516
589
  * All of these are inference-free, so they default to the management-call timeout.
@@ -601,13 +674,20 @@ export class PipelexApiClient {
601
674
  * Returns a **200 verdict**: pattern-match `is_valid` before reading the arm — a
602
675
  * closure that does not parse/load/validate comes back as `is_valid: false` with
603
676
  * `validation_errors[]`, not as a thrown error. Only a no-verdict condition throws
604
- * `ApiResponseError`: a malformed selector (neither or both of `files`/`method_ref`)
605
- * is a 422, and the reserved `method_ref` is a `501` until the server-side method
606
- * registry lands. To resolve a STORED method today, expand it first:
607
- * `resolve({ files: await client.getMethodClosure(methodId) })`.
677
+ * `ApiResponseError`: a malformed selector (none, or more than one, of
678
+ * `files` / `method_ref` / `method_id`) is a 422; a selector-resolution failure
679
+ * (fetch failure, no package at the address, an unknown or foreign-org id) is
680
+ * non-2xx too — never an `is_valid: false` verdict.
681
+ *
682
+ * The closure arrives in exactly one of three forms: inline `files`, an
683
+ * address-form `method_ref` (server-resolved, pipelex-api >= 0.21.0; the
684
+ * registry form stays a `501`), or a hosted `method_id` (platform-resolved —
685
+ * see {@link PipelexHostedToolingExtensions}).
608
686
  */
609
687
  async resolve(request) {
610
- return this.requestExtension("resolve", request);
688
+ return this.requestExtension("resolve", request, {
689
+ timeoutMs: crateRequestTimeoutMs(request),
690
+ });
611
691
  }
612
692
  /**
613
693
  * Project a closure's crate into stamped typed artifacts — `POST /v1/codegen`.
@@ -619,12 +699,15 @@ export class PipelexApiClient {
619
699
  * local `pipelex codegen types` run, so the offline `pipelex codegen check` passes on
620
700
  * it; the SDK deliberately does not write files for you.
621
701
  *
622
- * Same 200-verdict discipline as {@link resolve}. Only a no-verdict condition throws
623
- * `ApiResponseError`: an unknown `kind`/`target`, a `pipe_ref` on the concept-set-wide
624
- * `types` kind, or a malformed selector is a 422; `method_ref` is a `501`.
702
+ * Same 200-verdict discipline and same three-form closure selector as
703
+ * {@link resolve}. Only a no-verdict condition throws `ApiResponseError`: an
704
+ * unknown `kind`/`target`, a `pipe_ref` on the concept-set-wide `types` kind, or
705
+ * a malformed selector is a 422; a registry-form `method_ref` is a `501`.
625
706
  */
626
707
  async codegen(request) {
627
- return this.requestExtension("codegen", request);
708
+ return this.requestExtension("codegen", request, {
709
+ timeoutMs: crateRequestTimeoutMs(request),
710
+ });
628
711
  }
629
712
  // ── Build extensions (Pipelex API layer 2 — `/v1/build/*`) ────────
630
713
  /**
@@ -639,40 +722,32 @@ export class PipelexApiClient {
639
722
  * unresolvable closure comes back as `is_valid: false` with `validation_errors[]`,
640
723
  * not as a thrown error.
641
724
  *
642
- * Accepts the closure as inline `files` (the wire model {@link BuildInputsRequest}),
643
- * as a reserved `method_ref` (also on {@link BuildInputsRequest} — still `501`s,
644
- * same as `buildOutput`/`buildRunner`), or as a stored {@link BuildInputsByMethodId}
645
- * — the by-id form is resolved to `files` via {@link getMethodClosure} before the
646
- * request hits the wire (an empty source throws `EmptyMethodSourceError`; an
647
- * unknown/foreign id, the `getMethod` 404). Supply exactly one of the three.
725
+ * Accepts the closure as inline `files` or as a `method_ref` (both on the wire
726
+ * model {@link BuildInputsRequest}; the address form is server-resolved, the
727
+ * registry form `501`s) — exactly one of the two, like `buildOutput` /
728
+ * `buildRunner`. There is NO by-id form: the build routes take no `method_id`
729
+ * (the hosted tooling selector covers `validate`/`resolve`/`codegen` only), so a
730
+ * stored method is expanded first — `buildInputs({ files: await
731
+ * client.getMethodClosure(methodId) })`. That expansion stays the answer here
732
+ * because a `buildInputs` caller wants this route's template; it is not what
733
+ * `prepareInputs` does any more.
734
+ *
735
+ * Nothing inside this SDK calls this route: `prepareInputs` reads its signature
736
+ * from the input-form descriptor on the validate report. It survives for the
737
+ * consumers that still render a template over HTTP, and is retired with the rest
738
+ * of `/v1/build/*` once they project it client-side.
648
739
  */
649
740
  async buildInputs(request) {
650
- // The files/method_id either/or is a compile-time invariant (the two param
651
- // types are mutually exclusive); `resolveFilesOrMethodId` backs the
652
- // over-specified "both given" case at runtime for untyped (JS) callers —
653
- // shared with `resolveClosureFiles` in prepare-inputs.ts so this exact
654
- // check is defined once. `method_ref` is a third, still-legal closure
655
- // source (reserved, 501s server-side) shared with `buildOutput`/`buildRunner`
656
- // that prepareInputs does not have, so it stays local to this method: it
657
- // cannot combine with `method_id`, and a `method_ref`-only request must
658
- // reach the server like it does on those sibling routes rather than being
659
- // rejected as "neither given".
660
- const { method_id, files, method_ref } = request;
661
- if (method_id !== undefined && method_ref !== undefined) {
662
- throw new PipelineRequestError("buildInputs: `method_ref` cannot be combined with `method_id`.");
663
- }
664
- const resolvedFiles = await resolveFilesOrMethodId(files, method_id, (methodId) => this.getMethodClosure(methodId), (message) => new PipelineRequestError(`buildInputs: ${message}`));
665
- if (resolvedFiles === undefined && method_ref === undefined) {
666
- throw new PipelineRequestError("buildInputs: supply the closure as `files`, `method_id`, or `method_ref`.");
741
+ // `method_id` is `never` in the type; this backs it for untyped (JS)
742
+ // callers migrating off the retired client-side by-id expansion — a
743
+ // teaching error beats the server silently ignoring an unknown key.
744
+ if (request["method_id"] !== undefined) {
745
+ throw new PipelineRequestError("buildInputs takes no method_id — the build routes have no by-id form. Expand the stored " +
746
+ "method first: buildInputs({ files: await client.getMethodClosure(methodId) }).");
667
747
  }
668
- if (method_id !== undefined) {
669
- const { method_id: _methodId, ...rest } = request;
670
- return this.requestExtension("build/inputs", {
671
- ...rest,
672
- files: resolvedFiles,
673
- });
674
- }
675
- return this.requestExtension("build/inputs", request);
748
+ return this.requestExtension("build/inputs", request, {
749
+ timeoutMs: crateRequestTimeoutMs(request),
750
+ });
676
751
  }
677
752
  /**
678
753
  * Project a pipe's output concept — `POST /v1/build/output`. Same envelope and
@@ -681,7 +756,9 @@ export class PipelexApiClient {
681
756
  * text in `output_python`.
682
757
  */
683
758
  async buildOutput(request) {
684
- return this.requestExtension("build/output", request);
759
+ return this.requestExtension("build/output", request, {
760
+ timeoutMs: crateRequestTimeoutMs(request),
761
+ });
685
762
  }
686
763
  /**
687
764
  * Generate a runnable Python script plus its stamped typed structures —
@@ -947,6 +1024,13 @@ export class PipelexApiClient {
947
1024
  * with `methodSourceToContents`, and labels each resulting file with the
948
1025
  * `method_id` as its `source` provenance.
949
1026
  *
1027
+ * This is the LOCAL expansion utility — for callers that want the files in
1028
+ * hand (to edit, to diff, to feed a route with no by-id form, the `/v1/build/*`
1029
+ * family being the last of those). The operations that accept `method_id`
1030
+ * natively (`execute`/`start`, `validate`/`resolve`/`codegen`, and
1031
+ * `prepareInputs`, which composes a `validate` of its own) take the id as a
1032
+ * pass-through instead; nothing in this client expands an id behind your back.
1033
+ *
950
1034
  * Requires an API key: the methods catalog is org-scoped to the key's org, so
951
1035
  * an unknown OR foreign-org id is a `getMethod` `404` (`ApiResponseError`
952
1036
  * `not_found`), which propagates unchanged. A real, in-org method whose source
@@ -1096,10 +1180,20 @@ export class PipelexApiClient {
1096
1180
  * file-bearing assets, and return copy-on-write rewritten inputs (canonical
1097
1181
  * content carrying `pipelex-storage://` in `url`) plus one upload record per
1098
1182
  * prepared asset. HTTP(S) URLs and existing `pipelex-storage://` URIs pass
1099
- * through unchanged; all failures are raised before any run is created. The
1100
- * caller supplies the method closure EITHER as inline `files` OR as a stored
1101
- * `method_id` (resolved to its closure via `getMethodClosure` first; requires
1102
- * an API key). See `docs/input-preparation.md`.
1183
+ * through unchanged; all failures are raised before any run is created.
1184
+ *
1185
+ * Name the method exactly one of three ways, all server-resolved through the
1186
+ * one `validate` call this composes:
1187
+ *
1188
+ * - `files` — the inline MTHDS closure;
1189
+ * - `method_ref` — a published method's address, fetched by the runner;
1190
+ * - `method_id` — a stored method's catalog id, resolved by the platform
1191
+ * (hosted only; requires an API key).
1192
+ *
1193
+ * The signature itself is the input-form descriptor on the validate report
1194
+ * (`views: ["input_form", "output_form"]`), which states the kind of every input at every
1195
+ * depth — so a file position is a fact of the method, never a guess from the
1196
+ * value's shape. See `docs/input-preparation.md`.
1103
1197
  */
1104
1198
  async prepareInputs(request) {
1105
1199
  return prepareInputsImpl(this, request);
@@ -1204,12 +1298,14 @@ export class PipelexApiClient {
1204
1298
  }
1205
1299
  /**
1206
1300
  * Blocking `POST /v1/execute` adapted onto `RunResults` — the bare-runner
1207
- * path. Forwards every protocol field PLUS both extension surfaces: the
1208
- * hosted `method_id` and the generic `extra` passthrough. An extension-only
1209
- * call (`{ extra }` with no pipe_code/bundle) or a vendor selector riding
1210
- * `extra` must survive this path, not just the durable one — and a hosted
1211
- * `method_id` must reach the server here too, so a runner that cannot resolve
1212
- * it says so (Rule 4) instead of the client silently dropping it.
1301
+ * path. Forwards every protocol field PLUS every extension surface: the
1302
+ * runner-resolved `method_ref`, the hosted `method_id`, and the generic
1303
+ * `extra` passthrough. An extension-only call (`{ extra }` with no
1304
+ * pipe_code/bundle) or a vendor selector riding `extra` must survive this
1305
+ * path, not just the durable one — a `method_ref` run must run the same
1306
+ * fetched package here, and a hosted `method_id` must reach the server too,
1307
+ * so a runner that cannot resolve it says so (Rule 4) instead of the client
1308
+ * silently dropping it.
1213
1309
  */
1214
1310
  async executeBlocking(options) {
1215
1311
  const response = await this.execute({
@@ -1223,6 +1319,7 @@ export class PipelexApiClient {
1223
1319
  // this path runs the same method as the durable one, or it runs nothing.
1224
1320
  files: options.files ?? undefined,
1225
1321
  bundle_b64: options.bundle_b64 ?? undefined,
1322
+ method_ref: options.method_ref ?? undefined,
1226
1323
  method_id: options.method_id ?? undefined,
1227
1324
  extra: options.extra ?? undefined,
1228
1325
  });
@@ -1293,6 +1390,12 @@ const PROTOCOL_REQUEST_KEYS = [
1293
1390
  "files",
1294
1391
  "bundle_b64",
1295
1392
  ];
1393
+ // The PIPELEX API's own request fields — the layer-2 extension the runner
1394
+ // resolves itself (`method_ref`, a run source in its own right). Reserved on
1395
+ // `extra` for the same reason the protocol fields are: `extra` merges last, so
1396
+ // a smuggled copy would overwrite the validated named option and bypass the
1397
+ // selector-exclusivity checks.
1398
+ const PIPELEX_API_REQUEST_KEYS = ["method_ref"];
1296
1399
  // The HOSTED API's own request fields — the layer-3 extensions this client
1297
1400
  // names itself. Reserved on `extra` for the same reason the protocol fields
1298
1401
  // are: `extra` merges last, so a smuggled copy would overwrite the validated
@@ -1311,6 +1414,7 @@ const HOSTED_REQUEST_KEYS = ["method_id"];
1311
1414
  // reach the wire through the passthrough either.
1312
1415
  const RESERVED_EXTRA_KEYS = new Set([
1313
1416
  ...PROTOCOL_REQUEST_KEYS,
1417
+ ...PIPELEX_API_REQUEST_KEYS,
1314
1418
  ...HOSTED_REQUEST_KEYS,
1315
1419
  "bundleMain",
1316
1420
  ]);
@@ -1344,6 +1448,35 @@ function buildExtensions(extra) {
1344
1448
  }
1345
1449
  return result;
1346
1450
  }
1451
+ // `method_ref` resolution can make the server CLONE a repository before it
1452
+ // answers, and the server-side clone timeout runs well past the 30s static-route
1453
+ // budget on a cold cache. This budget covers that without touching the static
1454
+ // routes' no-transport-options policy (see the note on `requestExtension`): it
1455
+ // is an internal budget matched to what the server actually does, not a new
1456
+ // caller-facing parameter — and inert on the hosted path, where the gateway
1457
+ // caps responses regardless.
1458
+ const METHOD_REF_FETCH_TIMEOUT_MS = 180_000; // 3 min — covers the server's clone + resolve
1459
+ /**
1460
+ * The internal request budget for a call carrying a `CrateRequestBase` closure
1461
+ * (the crate routes and the build projections alike): the static-route default,
1462
+ * unless the closure is a `method_ref` the server may have to fetch first.
1463
+ * `buildRunner` is the exception — its own five-minute default already clears
1464
+ * the fetch budget.
1465
+ */
1466
+ function crateRequestTimeoutMs(request) {
1467
+ return nonEmptyString(request.method_ref) !== undefined ? METHOD_REF_FETCH_TIMEOUT_MS : undefined;
1468
+ }
1469
+ /**
1470
+ * Copy the Pipelex API's own run arguments onto the wire body. Named options,
1471
+ * not `extra` entries — see `PipelexApiRunExtensions`. Same emptiness rule as
1472
+ * the hosted builder below: an absent or empty `method_ref` yields an empty
1473
+ * object, so the key is not sent and does not count towards the "something to
1474
+ * run" precondition.
1475
+ */
1476
+ function buildApiRunExtensions(options) {
1477
+ const methodRef = nonEmptyString(options.method_ref);
1478
+ return methodRef === undefined ? {} : { method_ref: methodRef };
1479
+ }
1347
1480
  /**
1348
1481
  * Copy the hosted API's own run arguments onto the wire body. Named options,
1349
1482
  * not `extra` entries — see `PipelexHostedRunExtensions`.
@@ -1358,6 +1491,36 @@ function buildHostedRunExtensions(options) {
1358
1491
  const methodId = nonEmptyString(options.method_id);
1359
1492
  return methodId === undefined ? {} : { method_id: methodId };
1360
1493
  }
1494
+ /**
1495
+ * Enforce the run routes' `method_ref` exclusivity, mirroring the server's own
1496
+ * 422s so an illegal pairing fails before anything hits the wire. A
1497
+ * `method_ref` is a complete run source (the fetched package carries its
1498
+ * `.mthds` and its entry pipe), so it pairs with NOTHING: not with inline
1499
+ * `mthds_contents`, not with a method bundle, and not with the hosted
1500
+ * `method_id` — an address run has its own provenance and needs no linkage id.
1501
+ *
1502
+ * The one documented run-route exception is deliberately NOT here: inline
1503
+ * source + `method_id` stays legal (the inline source runs; the id demotes to
1504
+ * run-history linkage). `pipe_code` beside a `method_ref` is legal too — it
1505
+ * overrides the manifest's `main_pipe`. The wording of the first two errors
1506
+ * mirrors the server's validator; presence semantics match it as well
1507
+ * (`mthds_contents` counts when non-empty, a bundle encoding counts when the
1508
+ * key is present, the selectors count when non-empty).
1509
+ */
1510
+ function assertMethodRefPairsWithNothing(options) {
1511
+ if (nonEmptyString(options.method_ref) === undefined)
1512
+ return;
1513
+ if (options.mthds_contents != null && options.mthds_contents.length > 0) {
1514
+ throw new PipelineRequestError("method_ref and inline mthds_contents are mutually exclusive; send one or the other.");
1515
+ }
1516
+ if (options.files != null || options.bundle_b64 != null) {
1517
+ throw new PipelineRequestError("method_ref and a method bundle (bundle_b64 / files) are mutually exclusive; send one or the other.");
1518
+ }
1519
+ if (nonEmptyString(options.method_id) !== undefined) {
1520
+ throw new PipelineRequestError("method_ref and method_id are mutually exclusive: an address run carries its own provenance " +
1521
+ "and takes no run-history linkage id. Send exactly one method selector.");
1522
+ }
1523
+ }
1361
1524
  /**
1362
1525
  * Normalize a bundle encoding for the wire: an empty map / string is NOT a
1363
1526
  * runnable bundle, so it must not be sent (the runner rejects a zero-file