@pipelex/sdk 0.63.0 → 0.65.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/README.md CHANGED
@@ -178,6 +178,31 @@ const copy = await client.copyMethod(
178
178
  );
179
179
  if (copy.outcome === "copied") console.log(copy.method.copied_from); // the original it names
180
180
 
181
+ // Store a layout a producer designed for one pipe of a published version, over that version's
182
+ // tree; a write of the same tree, pipe, catalog and prompt hash replaces it. A validate by
183
+ // method_id naming `layouts` then carries the layouts beside the descriptor, newest first, and
184
+ // leaves `layouts` out when there are none: render the plain form then.
185
+ const v1 = await client.getMethodVersion(created.method_id, 1);
186
+ const layoutKey = crypto.randomUUID(); // one key per layout, sent again unchanged on each retry
187
+ await client.storeMethodLayout(created.method_id, 1, {
188
+ tree_digest: v1.tree_digest!,
189
+ pipe_ref: "greeting.greet",
190
+ catalog: "composer",
191
+ prompt_hash: "a1b2c3d4e5f6",
192
+ jsonl: '{"op":"add","path":"/root","value":"page"}',
193
+ provenance: { producer: "pipelex-method", model: "claude-4.8-opus", style: "editorial" },
194
+ }, { idempotencyKey: layoutKey });
195
+ const laidOut = await client.validate({ method_id: `${created.method_id}@1` }, false, {
196
+ views: ["input_form", "layouts"],
197
+ });
198
+ if (laidOut.is_valid) console.log(laidOut.layouts?.map((layout) => layout.catalog) ?? "plain form");
199
+
200
+ // A host opening the page reads the same layouts with no dry run, and a builder can switch a
201
+ // page's design off, per catalog, without losing it; the list is written whole.
202
+ const { items: laidOutPages } = await client.listMethodLayouts(created.method_id, 1);
203
+ console.log(laidOutPages.length);
204
+ await client.setMethodLayoutsOff(created.method_id, ["composer"]);
205
+
181
206
  // The template catalog: the public methods every organization reads, in the platform's order, with
182
207
  // the categories that have a template and each card's thumbnails behind short-lived links. A
183
208
  // category's `labels` name it by language tag and may lack a language, in which case a client
@@ -302,6 +327,7 @@ These pages ship inside the published package, so a reader who has only installe
302
327
  | [`docs/files.md`](./docs/files.md) | An organization's files: `listFiles` and `iterateFiles`, the row, the whole record and its provenance, the query and its repeated filters, paging through filtered pages, one file and its uses, the records of an address, the rename rule, links that name the file, a zip of files and its caps, a run's files, the copy into a method's draft, and an upload recorded as a method's test file with the refusals that come with it |
303
328
  | [`docs/method-examples.md`](./docs/method-examples.md) | A method's saved examples: the create, the paged list and its iterator, the edit that raises the input revision, the latest-run write and its `recorded` or `stale` verdict, the build assistant's save and its `kept` or `created` verdict, how an integer past `Number.MAX_SAFE_INTEGER` in the inputs stays exact, and what is refused before sending |
304
329
  | [`docs/method-copy.md`](./docs/method-copy.md) | Copying a method into another organization or duplicating it: what a copy holds and the stored files it copies, the `copied` or `refused` verdict, every refusal, the copy's idempotency key, and `copied_from` |
330
+ | [`docs/method-layouts.md`](./docs/method-layouts.md) | A method's stored layouts: `storeMethodLayout`, which stores a layout designed for one pipe of a published version, its closed body and its refusals, the layout, its provenance and its written sample, `listMethodLayouts`, which reads a version's layouts with no dry run, `setMethodLayoutsOff`, which switches a page's design off, and the `layouts` a validate by `method_id` carries when `views` names `layouts`, absent when there are none |
305
331
  | [`docs/public-methods.md`](./docs/public-methods.md) | Public methods and the template catalog: `listMethodTemplates` and its iterator, the ordering, the categories with their labels by language and `listMethodCategories`, the thumbnails' links, another organization's public method read in its narrower shape, the copy of a public method, `visibility` and `categories` on a method, and the refusals a public method's publish adds |
306
332
  | [`docs/organization-branding.md`](./docs/organization-branding.md) | An organization's logos, colours, app style and typeface, and its white-label entitlement: `getBranding`, which every member and API key reads, `updateOrganizationBranding`, a write by the `admin` or the `member` role that sets, clears or keeps each key, and how a refused logo, colour, style or typeface names its field so a settings screen shows the sentence under it |
307
333
  | [`docs/exact-integers.md`](./docs/exact-integers.md) | Which reads keep an integer past `Number.MAX_SAFE_INTEGER` exact, as a `bigint` (a run's inputs and outputs, an example's inputs), how a request body writes it back with its digits, and `parseExactJson` and `stringifyExactJson` for code showing such data as JSON text |
@@ -15,7 +15,7 @@ import type { CommandIO } from "./io.js";
15
15
  * update notice and npm 12's run notices off the script's stderr, which carries the command's
16
16
  * own lines, while npm's warnings and errors still print.
17
17
  */
18
- export declare const SCRIPT_RUNNER = "npx --yes --no-update-notifier --loglevel=warn @pipelex/sdk@0.63.0";
18
+ export declare const SCRIPT_RUNNER = "npx --yes --no-update-notifier --loglevel=warn @pipelex/sdk@0.65.0";
19
19
  export declare const SCRIPT_WRITER = "@pipelex/sdk";
20
20
  export declare const SCRIPT_NEEDS = "Node 22.12 or later";
21
21
  /** Run `pipelex-sdk script` and return its exit code. */
package/dist/client.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { MTHDSProtocol, ModelCategory, ModelDeck, RunOptions, StartOptions, VersionInfo } from "mthds/protocol";
2
- import type { CodegenRequest, CodegenResponse, CrateRouteOptions, DictPipeOutput, FormatResponse, LintResponse, ModelCheckCategory, ModelReferenceVerdict, PipeIORequest, PipeIOResponse, PipelexRunResultStart, PipelexValidateSource, PipelexValidationResult, ResolveRequest, ResolveResponse, ValidateMethodSelector } from "./models.js";
2
+ import type { CodegenRequest, CodegenResponse, CrateRouteOptions, DictPipeOutput, FormatResponse, LintResponse, ModelCheckCategory, ModelReferenceVerdict, PipeIORequest, PipeIOResponse, PipelexRunResultStart, PipelexValidateSource, PipelexValidationResult, ResolveRequest, ResolveResponse, ValidateMethodSelector, ValidateView } from "./models.js";
3
+ import type { MethodLayout, MethodLayoutInput, MethodLayoutList } from "./method-layouts.js";
3
4
  import { type GetRunResultOptions, type RunProgress, type RunRead, type RunResults, type RunResultState, type StartAndWaitForResultOptions, type WaitForResultOptions } from "./runs.js";
4
5
  import type { BillingPortalResponse, ChangePlanResponse, CheckoutResponse, DraftFileFromFileInput, DraftFileFromFileResult, FileArchive, FileArchiveRequest, FileDetail, FileLinkTarget, FileLinksResult, FilePage, FileRecord, FileRecordList, FileRenameInput, FileSummary, GetFileQuery, GetFilesByUriOptions, InvoiceView, Membership, MembershipsResponse, OrganizationBranding, OrganizationBrandingUpdate, ListFilesQuery, ListMethodExamplesQuery, ListMethodsQuery, ListMethodTemplatesQuery, ListMethodVersionsQuery, ListRunFilesQuery, MethodCategoryList, MethodCopyInput, MethodCopyResult, MethodData, MethodDeletionAccepted, MethodDetail, MethodDraftInput, MethodExample, MethodExampleAssistantSaveInput, MethodExampleAssistantSaveResult, MethodExampleCreateInput, MethodExampleLatestRunInput, MethodExampleLatestRunResult, MethodExamplePage, MethodExampleUpdateInput, MethodPage, MethodPublishInput, MethodPublishResult, MethodRenameInput, MethodSummary, MethodTemplate, MethodTemplatePage, MethodVersion, MethodVersionPage, MethodWriteInput, OnboardingSubmission, PublicMethodDetail, PipelexApiKeyCreated, PipelexApiKeyList, ListRunsQuery, RunHistoryItem, RunDetail, RunFilesPage, RunPage, PlanView, ResolvedStorageUrl, SavedMethodFile, SubscriptionResponse, UpdateRunInput, UploadGrant, UploadGrantInput, UploadInput, UploadedFile, UserProfile } from "./product-models.js";
5
6
  import type { MethodTreeForms, MethodTreeLimits } from "./method-trees.js";
@@ -173,6 +174,55 @@ export interface MethodCopyRequestOptions {
173
174
  */
174
175
  idempotencyKey?: string | null;
175
176
  }
177
+ /** Options for `listMethodLayouts`. */
178
+ export interface MethodLayoutListOptions {
179
+ /**
180
+ * Caller-driven cancellation, such as an `AbortSignal.timeout(…)` for a page that can do without
181
+ * the layouts; the abort reason propagates untouched.
182
+ */
183
+ signal?: AbortSignal;
184
+ }
185
+ /** Options for `setMethodLayoutsOff`. */
186
+ export interface MethodLayoutsOffOptions {
187
+ /**
188
+ * The write's idempotency key, sent as the `Idempotency-Key` header of
189
+ * `PUT /v1/methods/{id}/layouts-off`, and not sent at all when absent or `null`. Generate one per
190
+ * switch the caller means to make (a `crypto.randomUUID()` is a good key), and keep it, with the
191
+ * arguments unchanged, across that write's retries.
192
+ *
193
+ * The write is the whole list, so a write sent again without a key writes that list again, and
194
+ * undoes a switch that landed in between. Under the same key the hosted API answers the retry
195
+ * with the first write's stored answer and runs nothing. The rest of the rule, a key's form and
196
+ * its `409`s included, is `MethodLayoutWriteOptions.idempotencyKey`'s.
197
+ */
198
+ idempotencyKey?: string | null;
199
+ }
200
+ /** Options for `storeMethodLayout`. */
201
+ export interface MethodLayoutWriteOptions {
202
+ /**
203
+ * The write's idempotency key, sent as the `Idempotency-Key` header of
204
+ * `POST /v1/methods/{id}/versions/{n}/layouts`, and not sent at all when absent or `null`.
205
+ * Generate one per layout the caller means to store (a `crypto.randomUUID()` is a good key), and
206
+ * keep it, with the arguments unchanged, across that write's retries.
207
+ *
208
+ * A write sent again without a key runs again: it stamps the layout with a new `stored_at`, which
209
+ * moves it ahead of every layout stored since in the newest-first order, and applies the bound
210
+ * again, so a layout another producer stored meanwhile, a newer prompt of the same pipe and
211
+ * catalog included, can be the one removed. Under the same key the hosted API answers the retry
212
+ * with the first write's stored answer and runs nothing, so a write whose answer was lost, as on
213
+ * a client timeout, can be sent again unchanged. The write's `200` and its `4xx` refusals are
214
+ * stored alike; a `5xx` and a refusal for its timing (a `408`, `425` or `429`) are not, so the
215
+ * same key sends the write afresh. The same key with a different body is refused (`409`
216
+ * `request.idempotency_conflict`, whose `retry` is `no`), as is a key whose earlier request is
217
+ * still being handled (`409` `request.idempotency_key_in_use`, whose `retry` is `yes`); these
218
+ * two are the key's own answers and are never stored, so a retry after the second runs as the
219
+ * key's first request would.
220
+ *
221
+ * A key is held to the rule a start's is (`PipelexStartRequestOptions.idempotencyKey`): 1 to
222
+ * 255 visible ASCII characters, anything else a `RequestArgumentError` before any request.
223
+ */
224
+ idempotencyKey?: string | null;
225
+ }
176
226
  /**
177
227
  * `start()` / `startAndWaitForResult()` options — the same, for the durable path, plus the start
178
228
  * request's own `idempotencyKey`.
@@ -191,8 +241,13 @@ export interface ValidateOptions {
191
241
  * `"markdown"`, so both arms carry `rendered_markdown`.
192
242
  */
193
243
  render?: string[];
194
- /** Structured-view opt-in tokens, e.g. `["input_form", "output_form"]`; sent only when given. */
195
- views?: string[];
244
+ /**
245
+ * Structured-view opt-in tokens, e.g. `["input_form", "output_form"]`, or
246
+ * `["input_form", "layouts"]` for a stored method's descriptor and the layouts over it; sent
247
+ * verbatim, and only when given. A token this SDK does not know (`ValidateView`) is sent as its
248
+ * raw string.
249
+ */
250
+ views?: (ValidateView | (string & {}))[];
196
251
  /**
197
252
  * Per-call request ceiling; defaults to the 20-min execute ceiling. A positive number
198
253
  * no larger than 2147483647, the longest delay a timer honours, else a `RangeError`.
@@ -284,8 +339,8 @@ export declare class PipelexApiClient implements HostedProtocolSurface {
284
339
  * `url` is a fully resolved absolute URL. `mayStartRun` marks a request that may create a run,
285
340
  * whose errors then say whether it may have (see `RunCreation`).
286
341
  * `idempotencyKey`, already checked by `idempotencyKeyOf`, is sent as the
287
- * `Idempotency-Key` header; only `start`, `copyMethod`, `rotateWebhookSecret` and
288
- * `replayRunWebhookDelivery` pass one.
342
+ * `Idempotency-Key` header; only `start`, `copyMethod`, `storeMethodLayout`,
343
+ * `setMethodLayoutsOff`, `rotateWebhookSecret` and `replayRunWebhookDelivery` pass one.
289
344
  */
290
345
  private requestRaw;
291
346
  /**
@@ -315,7 +370,7 @@ export declare class PipelexApiClient implements HostedProtocolSurface {
315
370
  private requestProductObject;
316
371
  /**
317
372
  * A methods-catalog route answering one stored method (`createMethod`, `writeDraft`,
318
- * `renameMethod`), read as `readObjectAnswer` reads it.
373
+ * `renameMethod`, `setMethodLayoutsOff`), read as `readObjectAnswer` reads it.
319
374
  */
320
375
  private requestMethodData;
321
376
  /**
@@ -565,7 +620,10 @@ export declare class PipelexApiClient implements HostedProtocolSurface {
565
620
  * `options.views` is the sibling opt-in for *structured* views where `render`
566
621
  * carries *rendered text*. The two lists are independent, each resolving its own
567
622
  * tokens against its own supported set, and each supported token adds a same-named
568
- * top-level field to the valid arm — `input_form` and `output_form`. Unlike
623
+ * top-level field to the valid arm — `input_form` and `output_form`, and on a hosted
624
+ * validate by `method_id`, `layouts`, the layouts stored for the resolved tree
625
+ * (`PipelexValidationReport.layouts`, absent when there are none), each entry read as
626
+ * `storeMethodLayout`'s answer is and one that does not read as a layout left out. Unlike
569
627
  * `render`, this client sends `views` ONLY when the caller asks: the point of an
570
628
  * opt-in view is that the default response stays byte-identical, and the
571
629
  * highest-frequency consumers (hook pipelines, CI gates, agent loops) never pay for
@@ -1372,6 +1430,103 @@ export declare class PipelexApiClient implements HostedProtocolSurface {
1372
1430
  * one, which names no tree to serve.
1373
1431
  */
1374
1432
  getMethodVersion(methodId: string, version: number): Promise<MethodVersion>;
1433
+ /**
1434
+ * Store a layout designed for one pipe of a published version of a method —
1435
+ * `POST /v1/methods/{id}/versions/{n}/layouts` — and answer it as stored, with the `stored_at` the
1436
+ * platform stamped.
1437
+ *
1438
+ * A layout is a data file a producer designed over the version's input-form descriptor, in the
1439
+ * vocabulary of a catalog of components. The platform stores it beside the method, keyed by the
1440
+ * content it was designed for, and hands it back through `listMethodLayouts` and on every validate
1441
+ * of the method by `method_id` that names `layouts` among its `views` and resolves the same tree
1442
+ * (`PipelexValidationReport.layouts`). It never produces, parses or judges one.
1443
+ *
1444
+ * `layout.tree_digest` must be the digest of the tree `version` holds, its
1445
+ * `MethodVersionSummary.tree_digest`: a producer designs over the descriptor of the version it was
1446
+ * handed. **A write of the same identity replaces the layout**, the identity being `tree_digest`,
1447
+ * `pipe_ref`, `catalog` and `prompt_hash`, and is answered the same `200` whether it stored a new
1448
+ * layout or replaced one. `options.idempotencyKey` is sent as the `Idempotency-Key` header, so a
1449
+ * write whose answer was lost is sent again under the same key and answered as the first was; sent
1450
+ * again without one, it runs again, with a new `stored_at` and the bound applied again
1451
+ * (`MethodLayoutWriteOptions`). A content keeps at most 16
1452
+ * layouts, every pipe, catalog and prompt hash together; past the bound a write removes first what
1453
+ * newer prompts of the same pipe and catalog superseded (see `docs/method-layouts.md`).
1454
+ * `layout.sample_json` keeps a written sample with the layout, never a run's output.
1455
+ *
1456
+ * The write changes the method, so it takes an `admin` or a `member` of the method's
1457
+ * organization. Takes a bare catalog id, as `getMethod` does, and the version number. Sends the
1458
+ * members of `MethodLayoutInput` and nothing else, the route's body being closed at both levels,
1459
+ * and leaves out `sample_json` and each optional provenance member that is `null` or omitted, for
1460
+ * the platform's default. Throws `RequestArgumentError` before any request for a `version` that is not a positive
1461
+ * integer and for a `layout` or a `provenance` that is not an object; every other member is sent
1462
+ * as given. Throws `ApiResponseError` when the platform refuses the write, which stores nothing:
1463
+ * `422` `request.invalid` for a member breaking its shape or bound, naming it in `params.field`
1464
+ * and `location.field` (`provenance.<member>` for one of the provenance's), and, naming
1465
+ * `tree_digest`, for a digest that is not the tree the version holds; `403` `auth.forbidden`
1466
+ * naming the `member` requirement for a `runner` or a caller with no recognised role; `404`
1467
+ * `pipelex.catalog.not_found` for an unknown method or another organization's, its
1468
+ * `params.resource` `method`, and for a version the method never published, its
1469
+ * `params.resource` `version`; `409` `pipelex.catalog.being_deleted` while the method's erasure is
1470
+ * pending, under way or stopped; and `500` `runtime.unexpected` for a version stored before
1471
+ * methods were trees, which names no tree. Under an idempotency key, `409`
1472
+ * `request.idempotency_conflict` for the key sent with a different body and `409`
1473
+ * `request.idempotency_key_in_use` while its earlier request is still being handled; a key that is
1474
+ * not 1 to 255 visible ASCII characters is a `RequestArgumentError` before any request. An answer
1475
+ * that does not read as a layout (`methodLayoutFault`) is an answer the SDK cannot read, thrown as
1476
+ * an `ApiResponseError` too.
1477
+ */
1478
+ storeMethodLayout(methodId: string, version: number, layout: MethodLayoutInput, options?: MethodLayoutWriteOptions): Promise<MethodLayout>;
1479
+ /**
1480
+ * The layouts stored for the content of a published version of a method —
1481
+ * `GET /v1/methods/{id}/versions/{n}/layouts` — newest first, at most 16, `items` empty when none
1482
+ * is stored. It touches only the platform's database and calls no runner, so it costs a host far
1483
+ * less than the `layouts` view of `validate`, which dry-runs every pipe of the method.
1484
+ *
1485
+ * It answers what is stored whatever `layouts_off` says: the switch is the host's to honour.
1486
+ * Another organization's public method reads too, with the layouts its builders stored, and a
1487
+ * `runner` reads what it is shown. Takes a bare catalog id, as `getMethod` does, and the version
1488
+ * number. `options.signal` cancels the read. Throws `RequestArgumentError` for a `version` that is
1489
+ * not a positive integer, before any request.
1490
+ *
1491
+ * Throws `ApiResponseError`: `404` `pipelex.catalog.not_found` for an unknown method or another
1492
+ * organization's internal one, its `params.resource` `method`, and for a version the method never
1493
+ * published, its `params.resource` `version`; `409` `pipelex.catalog.being_deleted` while the
1494
+ * erasure of the caller's own method is pending, under way or stopped; `500` `runtime.unexpected`
1495
+ * for a version stored before methods were trees, which names no tree. An answer whose `items` is
1496
+ * not an array is an answer the SDK cannot read, thrown as an `ApiResponseError` too; an entry
1497
+ * that does not read as a layout is left out (`readMethodLayouts`), so one layout it cannot read
1498
+ * never costs the caller the others.
1499
+ */
1500
+ listMethodLayouts(methodId: string, version: number, options?: MethodLayoutListOptions): Promise<MethodLayoutList>;
1501
+ /**
1502
+ * Set which catalogs of a method have their layouts switched off —
1503
+ * `PUT /v1/methods/{id}/layouts-off` — and return the method, its `layouts_off` the list as held.
1504
+ *
1505
+ * `catalogs` is the whole list and replaces the one held, each catalog id once, at most 8, empty
1506
+ * when every design is on: to change one, read `layouts_off` and write the new list. The switch
1507
+ * acts at once, with no publish, and holds across every publish. A layout switched off is kept,
1508
+ * and `listMethodLayouts` still answers it; a host draws the plain page instead. Like a rename, it
1509
+ * never moves the draft token, so the `updated_at` a caller holds stays valid.
1510
+ *
1511
+ * The last write to land is the one held. `options.idempotencyKey` is sent as the
1512
+ * `Idempotency-Key` header, so a write whose answer was lost is sent again under the same key and
1513
+ * answered as the first was (`MethodLayoutsOffOptions`).
1514
+ *
1515
+ * The write changes the method, so it takes an `admin` or a `member` of the method's
1516
+ * organization. Takes a bare catalog id, as `getMethod` does. Throws `RequestArgumentError` before
1517
+ * any request for `catalogs` that is not an array; its entries are sent as given. Throws
1518
+ * `ApiResponseError`: `404` `pipelex.catalog.not_found` for an unknown method or another
1519
+ * organization's, a public one included; `403` `auth.forbidden` naming the `member` requirement
1520
+ * for a `runner` or a caller with no recognised role; `409` `pipelex.catalog.being_deleted` while
1521
+ * the method's erasure is pending, under way or stopped; `413` `request.too_large` for a list that
1522
+ * would leave the method too large to publish, which a list that does not grow never is; and
1523
+ * `422` `request.invalid` for more than 8 catalogs, a catalog named twice or one that is not a
1524
+ * slug. Under an idempotency key, `409` `request.idempotency_conflict` for the key sent with a
1525
+ * different list and `409` `request.idempotency_key_in_use` while its earlier request is still
1526
+ * being handled; a key that is not 1 to 255 visible ASCII characters is a `RequestArgumentError`
1527
+ * before any request.
1528
+ */
1529
+ setMethodLayoutsOff(methodId: string, catalogs: readonly string[], options?: MethodLayoutsOffOptions): Promise<MethodData>;
1375
1530
  /**
1376
1531
  * Copy a method into an organization the caller may change methods in —
1377
1532
  * `POST /v1/methods/{id}/copy`: the method's own organization for a duplicate, another team, or
package/dist/client.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // source combinations are legal is an invariant of `RunRequest` itself, so this
3
3
  // client and the MTHDS runners cannot disagree about what they reject.
4
4
  import { assertOneMethodSource, buildProblem, isProblem, parseJsonBody, readProblemBody, } from "mthds/protocol";
5
+ import { LAYOUT_INPUT_MEMBERS, PROVENANCE_INPUT_MEMBERS, methodLayoutFault, readMethodLayout, readMethodLayouts, } from "./method-layouts.js";
5
6
  import { assertArtifactSelection, assertWaitOptions, pollUntilResult, readRunDegradation, RUN_STATUSES, runProgressFault, runStatusFilter, selectionIncludesMainStuff, throwIfAborted, withCheckedProgressHealth, } from "./runs.js";
6
7
  import { ABORT_TIMEOUT, ApiResponseError, argumentInvalidProblem, ApiUnreachableError, MissingMainStuffError, PagingNotTerminatingError, PipelineExecuteTimeoutError, ProblemError, RequestArgumentError, RunLifecycleUnavailableError, RunStillRunningError, } from "./errors.js";
7
8
  import { isPlainObject } from "./records.js";
@@ -326,6 +327,27 @@ const COPY_DRAFT_TOKEN_ARGUMENT = {
326
327
  what: "an expected_draft_updated_at, the draft token the caller last read, the method's " +
327
328
  "`updated_at`, a string, or none",
328
329
  };
330
+ /** What each argument of a layout write is, as a refusal of it names it. */
331
+ const LAYOUT_VERSION_ARGUMENT = {
332
+ name: "version",
333
+ what: "a version number, the published version the layout was designed for, a positive integer",
334
+ };
335
+ const LAYOUT_LIST_VERSION_ARGUMENT = {
336
+ name: "version",
337
+ what: "a version number, the published version whose layouts to read, a positive integer",
338
+ };
339
+ const LAYOUTS_OFF_CATALOGS_ARGUMENT = {
340
+ name: "catalogs",
341
+ what: "the ids of the catalogs to switch off as an array, the whole list, empty for none",
342
+ };
343
+ const LAYOUT_ARGUMENT = {
344
+ name: "layout",
345
+ what: "the layout to store as an object, a MethodLayoutInput",
346
+ };
347
+ const LAYOUT_PROVENANCE_ARGUMENT = {
348
+ name: "provenance",
349
+ what: "the layout's provenance as an object naming at least its producer and its model",
350
+ };
329
351
  /**
330
352
  * The refusal of an argument a call cannot take, worded as every such refusal is:
331
353
  * `<call>() takes <what>; got <got>.`, with what made it unusable as `cause` when there is one.
@@ -374,12 +396,16 @@ function requireNameAndInputs(name, inputs, call, needs) {
374
396
  }
375
397
  /**
376
398
  * A caller's value as the refusal of a number argument names it: a number by its value, anything
377
- * else by its type, as `requireNonEmptyId` names a value, and `null` as itself. Never by `String`,
378
- * which throws for an object with no prototype and would replace the refusal with a `TypeError`.
399
+ * else by its type, as `requireNonEmptyId` names a value, `null` as itself and an array as one, so
400
+ * a refusal of a value that is not an object never answers an array with "got object". Never by
401
+ * `String`, which throws for an object with no prototype and would replace the refusal with a
402
+ * `TypeError`.
379
403
  */
380
404
  function describeNumberArgument(value) {
381
405
  if (typeof value === "number")
382
406
  return String(value);
407
+ if (Array.isArray(value))
408
+ return "an array";
383
409
  return value === null ? "null" : typeof value;
384
410
  }
385
411
  /**
@@ -582,8 +608,8 @@ export class PipelexApiClient {
582
608
  * `url` is a fully resolved absolute URL. `mayStartRun` marks a request that may create a run,
583
609
  * whose errors then say whether it may have (see `RunCreation`).
584
610
  * `idempotencyKey`, already checked by `idempotencyKeyOf`, is sent as the
585
- * `Idempotency-Key` header; only `start`, `copyMethod`, `rotateWebhookSecret` and
586
- * `replayRunWebhookDelivery` pass one.
611
+ * `Idempotency-Key` header; only `start`, `copyMethod`, `storeMethodLayout`,
612
+ * `setMethodLayoutsOff`, `rotateWebhookSecret` and `replayRunWebhookDelivery` pass one.
587
613
  */
588
614
  async requestRaw(method, url, options = {}) {
589
615
  const hasBody = options.body !== undefined;
@@ -700,7 +726,7 @@ export class PipelexApiClient {
700
726
  }
701
727
  /**
702
728
  * A methods-catalog route answering one stored method (`createMethod`, `writeDraft`,
703
- * `renameMethod`), read as `readObjectAnswer` reads it.
729
+ * `renameMethod`, `setMethodLayoutsOff`), read as `readObjectAnswer` reads it.
704
730
  */
705
731
  async requestMethodData(method, endpoint, body, options = {}) {
706
732
  const res = await this.requestProductAnswer(method, endpoint, body, options);
@@ -1181,7 +1207,10 @@ export class PipelexApiClient {
1181
1207
  * `options.views` is the sibling opt-in for *structured* views where `render`
1182
1208
  * carries *rendered text*. The two lists are independent, each resolving its own
1183
1209
  * tokens against its own supported set, and each supported token adds a same-named
1184
- * top-level field to the valid arm — `input_form` and `output_form`. Unlike
1210
+ * top-level field to the valid arm — `input_form` and `output_form`, and on a hosted
1211
+ * validate by `method_id`, `layouts`, the layouts stored for the resolved tree
1212
+ * (`PipelexValidationReport.layouts`, absent when there are none), each entry read as
1213
+ * `storeMethodLayout`'s answer is and one that does not read as a layout left out. Unlike
1185
1214
  * `render`, this client sends `views` ONLY when the caller asks: the point of an
1186
1215
  * opt-in view is that the default response stays byte-identical, and the
1187
1216
  * highest-frequency consumers (hook pipelines, CI gates, agent loops) never pay for
@@ -1239,7 +1268,7 @@ export class PipelexApiClient {
1239
1268
  if (res.status < 200 || res.status >= 300) {
1240
1269
  this.throwApiResponseError("POST", "validate", res);
1241
1270
  }
1242
- return withVerdictItems(this.readObjectAnswer("POST", "validate", res));
1271
+ return withReadLayouts(withVerdictItems(this.readObjectAnswer("POST", "validate", res)));
1243
1272
  }
1244
1273
  // ── Tools extensions (Pipelex API — `/v1/lint`, `/v1/format`) ─────────
1245
1274
  //
@@ -2635,6 +2664,140 @@ export class PipelexApiClient {
2635
2664
  const res = await this.requestProductAnswer("GET", endpoint, undefined);
2636
2665
  return this.readSavedTree("GET", endpoint, withLinks(res), "a published version");
2637
2666
  }
2667
+ /**
2668
+ * Store a layout designed for one pipe of a published version of a method —
2669
+ * `POST /v1/methods/{id}/versions/{n}/layouts` — and answer it as stored, with the `stored_at` the
2670
+ * platform stamped.
2671
+ *
2672
+ * A layout is a data file a producer designed over the version's input-form descriptor, in the
2673
+ * vocabulary of a catalog of components. The platform stores it beside the method, keyed by the
2674
+ * content it was designed for, and hands it back through `listMethodLayouts` and on every validate
2675
+ * of the method by `method_id` that names `layouts` among its `views` and resolves the same tree
2676
+ * (`PipelexValidationReport.layouts`). It never produces, parses or judges one.
2677
+ *
2678
+ * `layout.tree_digest` must be the digest of the tree `version` holds, its
2679
+ * `MethodVersionSummary.tree_digest`: a producer designs over the descriptor of the version it was
2680
+ * handed. **A write of the same identity replaces the layout**, the identity being `tree_digest`,
2681
+ * `pipe_ref`, `catalog` and `prompt_hash`, and is answered the same `200` whether it stored a new
2682
+ * layout or replaced one. `options.idempotencyKey` is sent as the `Idempotency-Key` header, so a
2683
+ * write whose answer was lost is sent again under the same key and answered as the first was; sent
2684
+ * again without one, it runs again, with a new `stored_at` and the bound applied again
2685
+ * (`MethodLayoutWriteOptions`). A content keeps at most 16
2686
+ * layouts, every pipe, catalog and prompt hash together; past the bound a write removes first what
2687
+ * newer prompts of the same pipe and catalog superseded (see `docs/method-layouts.md`).
2688
+ * `layout.sample_json` keeps a written sample with the layout, never a run's output.
2689
+ *
2690
+ * The write changes the method, so it takes an `admin` or a `member` of the method's
2691
+ * organization. Takes a bare catalog id, as `getMethod` does, and the version number. Sends the
2692
+ * members of `MethodLayoutInput` and nothing else, the route's body being closed at both levels,
2693
+ * and leaves out `sample_json` and each optional provenance member that is `null` or omitted, for
2694
+ * the platform's default. Throws `RequestArgumentError` before any request for a `version` that is not a positive
2695
+ * integer and for a `layout` or a `provenance` that is not an object; every other member is sent
2696
+ * as given. Throws `ApiResponseError` when the platform refuses the write, which stores nothing:
2697
+ * `422` `request.invalid` for a member breaking its shape or bound, naming it in `params.field`
2698
+ * and `location.field` (`provenance.<member>` for one of the provenance's), and, naming
2699
+ * `tree_digest`, for a digest that is not the tree the version holds; `403` `auth.forbidden`
2700
+ * naming the `member` requirement for a `runner` or a caller with no recognised role; `404`
2701
+ * `pipelex.catalog.not_found` for an unknown method or another organization's, its
2702
+ * `params.resource` `method`, and for a version the method never published, its
2703
+ * `params.resource` `version`; `409` `pipelex.catalog.being_deleted` while the method's erasure is
2704
+ * pending, under way or stopped; and `500` `runtime.unexpected` for a version stored before
2705
+ * methods were trees, which names no tree. Under an idempotency key, `409`
2706
+ * `request.idempotency_conflict` for the key sent with a different body and `409`
2707
+ * `request.idempotency_key_in_use` while its earlier request is still being handled; a key that is
2708
+ * not 1 to 255 visible ASCII characters is a `RequestArgumentError` before any request. An answer
2709
+ * that does not read as a layout (`methodLayoutFault`) is an answer the SDK cannot read, thrown as
2710
+ * an `ApiResponseError` too.
2711
+ */
2712
+ async storeMethodLayout(methodId, version, layout, options = {}) {
2713
+ const call = "storeMethodLayout";
2714
+ if (!Number.isSafeInteger(version) || version < 1) {
2715
+ throw argumentRefusal(call, LAYOUT_VERSION_ARGUMENT, describeNumberArgument(version), "not_a_positive_integer");
2716
+ }
2717
+ const endpoint = `${methodPath(methodId, call)}/versions/${version}/layouts`;
2718
+ const body = methodLayoutBody(layout, call);
2719
+ const idempotencyKey = idempotencyKeyOf(options, "store the layout");
2720
+ const res = await this.requestProductAnswer("POST", endpoint, body, { idempotencyKey });
2721
+ const answer = this.readObjectAnswer("POST", endpoint, res);
2722
+ const stored = readMethodLayout(answer);
2723
+ if (stored === null) {
2724
+ throw this.unreadableAnswer("POST", `/${API_PREFIX}/${endpoint}`, res, `a layout ${methodLayoutFault(answer) ?? "that is not one"}`);
2725
+ }
2726
+ return stored;
2727
+ }
2728
+ /**
2729
+ * The layouts stored for the content of a published version of a method —
2730
+ * `GET /v1/methods/{id}/versions/{n}/layouts` — newest first, at most 16, `items` empty when none
2731
+ * is stored. It touches only the platform's database and calls no runner, so it costs a host far
2732
+ * less than the `layouts` view of `validate`, which dry-runs every pipe of the method.
2733
+ *
2734
+ * It answers what is stored whatever `layouts_off` says: the switch is the host's to honour.
2735
+ * Another organization's public method reads too, with the layouts its builders stored, and a
2736
+ * `runner` reads what it is shown. Takes a bare catalog id, as `getMethod` does, and the version
2737
+ * number. `options.signal` cancels the read. Throws `RequestArgumentError` for a `version` that is
2738
+ * not a positive integer, before any request.
2739
+ *
2740
+ * Throws `ApiResponseError`: `404` `pipelex.catalog.not_found` for an unknown method or another
2741
+ * organization's internal one, its `params.resource` `method`, and for a version the method never
2742
+ * published, its `params.resource` `version`; `409` `pipelex.catalog.being_deleted` while the
2743
+ * erasure of the caller's own method is pending, under way or stopped; `500` `runtime.unexpected`
2744
+ * for a version stored before methods were trees, which names no tree. An answer whose `items` is
2745
+ * not an array is an answer the SDK cannot read, thrown as an `ApiResponseError` too; an entry
2746
+ * that does not read as a layout is left out (`readMethodLayouts`), so one layout it cannot read
2747
+ * never costs the caller the others.
2748
+ */
2749
+ async listMethodLayouts(methodId, version, options = {}) {
2750
+ const call = "listMethodLayouts";
2751
+ if (!Number.isSafeInteger(version) || version < 1) {
2752
+ throw argumentRefusal(call, LAYOUT_LIST_VERSION_ARGUMENT, describeNumberArgument(version), "not_a_positive_integer");
2753
+ }
2754
+ const endpoint = `${methodPath(methodId, call)}/versions/${version}/layouts`;
2755
+ const res = await this.requestProductAnswer("GET", endpoint, undefined, {
2756
+ signal: options.signal,
2757
+ });
2758
+ const answer = this.readObjectAnswer("GET", endpoint, res);
2759
+ if (!Array.isArray(answer.items)) {
2760
+ throw this.unreadableAnswer("GET", `/${API_PREFIX}/${endpoint}`, res, "a list of layouts whose `items` is not an array");
2761
+ }
2762
+ return { ...answer, items: readMethodLayouts(answer.items) };
2763
+ }
2764
+ /**
2765
+ * Set which catalogs of a method have their layouts switched off —
2766
+ * `PUT /v1/methods/{id}/layouts-off` — and return the method, its `layouts_off` the list as held.
2767
+ *
2768
+ * `catalogs` is the whole list and replaces the one held, each catalog id once, at most 8, empty
2769
+ * when every design is on: to change one, read `layouts_off` and write the new list. The switch
2770
+ * acts at once, with no publish, and holds across every publish. A layout switched off is kept,
2771
+ * and `listMethodLayouts` still answers it; a host draws the plain page instead. Like a rename, it
2772
+ * never moves the draft token, so the `updated_at` a caller holds stays valid.
2773
+ *
2774
+ * The last write to land is the one held. `options.idempotencyKey` is sent as the
2775
+ * `Idempotency-Key` header, so a write whose answer was lost is sent again under the same key and
2776
+ * answered as the first was (`MethodLayoutsOffOptions`).
2777
+ *
2778
+ * The write changes the method, so it takes an `admin` or a `member` of the method's
2779
+ * organization. Takes a bare catalog id, as `getMethod` does. Throws `RequestArgumentError` before
2780
+ * any request for `catalogs` that is not an array; its entries are sent as given. Throws
2781
+ * `ApiResponseError`: `404` `pipelex.catalog.not_found` for an unknown method or another
2782
+ * organization's, a public one included; `403` `auth.forbidden` naming the `member` requirement
2783
+ * for a `runner` or a caller with no recognised role; `409` `pipelex.catalog.being_deleted` while
2784
+ * the method's erasure is pending, under way or stopped; `413` `request.too_large` for a list that
2785
+ * would leave the method too large to publish, which a list that does not grow never is; and
2786
+ * `422` `request.invalid` for more than 8 catalogs, a catalog named twice or one that is not a
2787
+ * slug. Under an idempotency key, `409` `request.idempotency_conflict` for the key sent with a
2788
+ * different list and `409` `request.idempotency_key_in_use` while its earlier request is still
2789
+ * being handled; a key that is not 1 to 255 visible ASCII characters is a `RequestArgumentError`
2790
+ * before any request.
2791
+ */
2792
+ async setMethodLayoutsOff(methodId, catalogs, options = {}) {
2793
+ const call = "setMethodLayoutsOff";
2794
+ const endpoint = `${methodPath(methodId, call)}/layouts-off`;
2795
+ if (!Array.isArray(catalogs)) {
2796
+ throw argumentRefusal(call, LAYOUTS_OFF_CATALOGS_ARGUMENT, describeNumberArgument(catalogs), "not_a_list");
2797
+ }
2798
+ const idempotencyKey = idempotencyKeyOf(options, "set the layouts off");
2799
+ return this.requestMethodData("PUT", endpoint, { catalogs }, { idempotencyKey });
2800
+ }
2638
2801
  /**
2639
2802
  * Copy a method into an organization the caller may change methods in —
2640
2803
  * `POST /v1/methods/{id}/copy`: the method's own organization for a duplicate, another team, or
@@ -4402,13 +4565,14 @@ const MAX_IDEMPOTENCY_KEY_LENGTH = 255;
4402
4565
  */
4403
4566
  const VISIBLE_ASCII = /^[\x21-\x7E]+$/;
4404
4567
  /**
4405
- * The idempotency key a start, a method copy, a secret rotation or a delivery replay sends, or
4568
+ * The idempotency key a start, a method copy, a layout write, a write of the layouts switched off,
4569
+ * a secret rotation or a delivery replay sends, or
4406
4570
  * `undefined` when the options carry none (absent or `null`). A key that is not a string of 1 to
4407
4571
  * 255 visible ASCII characters is refused with a `RequestArgumentError` rather than dropped, an
4408
4572
  * empty one included, since a request sent without the key its caller gave would lose the
4409
4573
  * protection the caller asked for without a word. `request` says what the request does, as the
4410
- * refusal's last sentence names it ("start the run", "copy the method", "rotate the secret",
4411
- * "replay the delivery"). The message never quotes the key, which a caller may derive from
4574
+ * refusal's last sentence names it ("start the run", "copy the method", "store the layout",
4575
+ * "set the layouts off", "rotate the secret", "replay the delivery"). The message never quotes the key, which a caller may derive from
4412
4576
  * something private. The Python SDK holds a key to the same rule, in the same words.
4413
4577
  */
4414
4578
  function idempotencyKeyOf(options, request = "start the run") {
@@ -4771,6 +4935,54 @@ function withVerdictItems(answer) {
4771
4935
  read.warnings = readProblemItems(answer.warnings);
4772
4936
  return read;
4773
4937
  }
4938
+ /**
4939
+ * A valid verdict with its `layouts` read entry by entry (`readMethodLayouts`), only when the
4940
+ * answer carries the key: each entry that reads as a layout is kept, in the answer's order, and a
4941
+ * list left with none is removed, so `layouts` is either absent or holds at least one layout, as
4942
+ * the platform sends it. Any other answer is returned as it came.
4943
+ */
4944
+ function withReadLayouts(answer) {
4945
+ if (!isPlainObject(answer) || answer.is_valid !== true || !Object.hasOwn(answer, "layouts")) {
4946
+ return answer;
4947
+ }
4948
+ const read = { ...answer };
4949
+ const layouts = readMethodLayouts(answer.layouts);
4950
+ if (layouts.length === 0)
4951
+ delete read.layouts;
4952
+ else
4953
+ read.layouts = layouts;
4954
+ return read;
4955
+ }
4956
+ /** `source`'s members that `members` lists, each `optional` one left out when `null` or omitted. */
4957
+ function sentMembers(members, source) {
4958
+ const sent = {};
4959
+ for (const [member, presence] of Object.entries(members)) {
4960
+ const value = source[member];
4961
+ if (presence === "required" || value != null)
4962
+ sent[member] = value;
4963
+ }
4964
+ return sent;
4965
+ }
4966
+ /**
4967
+ * The body `storeMethodLayout` sends for `layout`: the members of `MethodLayoutInput` and nothing
4968
+ * else, since the route's body is closed at both levels, `sample_json` and each provenance member
4969
+ * that is `null` or omitted left out for the platform's default. The members are `LAYOUT_INPUT_MEMBERS` and
4970
+ * `PROVENANCE_INPUT_MEMBERS`, which the build holds to the interfaces. A layout or a provenance that is not an object is
4971
+ * refused with `RequestArgumentError` before anything is sent; every other member is sent as given,
4972
+ * for the platform to hold to its shapes, its `422` naming the member it refuses.
4973
+ */
4974
+ function methodLayoutBody(layout, call) {
4975
+ if (!isPlainObject(layout)) {
4976
+ throw argumentRefusal(call, LAYOUT_ARGUMENT, describeNumberArgument(layout), "not_an_object");
4977
+ }
4978
+ const provenance = layout.provenance;
4979
+ if (!isPlainObject(provenance)) {
4980
+ throw argumentRefusal(call, LAYOUT_PROVENANCE_ARGUMENT, describeNumberArgument(provenance), "not_an_object");
4981
+ }
4982
+ const body = sentMembers(LAYOUT_INPUT_MEMBERS, layout);
4983
+ body.provenance = sentMembers(PROVENANCE_INPUT_MEMBERS, provenance);
4984
+ return body;
4985
+ }
4774
4986
  /**
4775
4987
  * A run record with its `error` kept only when it is a problem document, and read as `null`
4776
4988
  * otherwise. Only when the record carries the `error` key, so a server that does not serve it