@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 +26 -0
- package/dist/cli/script.d.ts +1 -1
- package/dist/client.d.ts +162 -7
- package/dist/client.js +222 -10
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js.map +1 -1
- package/dist/method-layouts.d.ts +195 -0
- package/dist/method-layouts.js +121 -0
- package/dist/method-layouts.js.map +1 -0
- package/dist/models.d.ts +27 -0
- package/dist/product-models.d.ts +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +3 -1
- package/docs/errors.md +2 -0
- package/docs/method-layouts.md +168 -0
- package/package.json +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# A method's stored layouts
|
|
2
|
+
|
|
3
|
+
A **layout** is a data file a producer designed for one pipe of a method: a tree of elements written in the vocabulary of a catalog of components, every value bound to a path in the host's form state, carrying no field descriptions and no data of its own. A host renders it over the input-form descriptor the same method's validate projects, and renders the plain form instead whenever it cannot. The hosted platform stores layouts beside a method's published versions and hands them back, on a direct read by the version or on a validate of the method by id, so a method laid out once opens with its layout for everyone who opens it. It never produces a layout, never parses one and never judges one against a catalog or a descriptor: that is the producer's work before the write and the host's before it renders.
|
|
4
|
+
|
|
5
|
+
The client reaches the write on the hosted API's `POST /v1/methods/{method_id}/versions/{version}/layouts`, the direct read on `GET` of the same route, the read through `POST /v1/validate` under the `layouts` view, and the off switch on `PUT /v1/methods/{method_id}/layouts-off`. The types are in `src/method-layouts.ts`, exported from the package's root: `MethodLayoutInput` and `LayoutProvenanceInput` for the write, `MethodLayout` and `LayoutProvenance` for what is stored, and `MethodLayoutList` for the direct read. The storage and the `layouts` token are a Pipelex hosted feature; the shapes are stated brand-neutrally, snake_case with no `pipelex_` prefix, as the input-form descriptor they are rendered over is. The conformance spec `pipelex-hosted-method-layouts.md`, in the workspace's `conformance` repository, states the contract.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { PipelexApiClient } from "@pipelex/sdk";
|
|
9
|
+
import type { MethodLayout } from "@pipelex/sdk";
|
|
10
|
+
|
|
11
|
+
const client = new PipelexApiClient();
|
|
12
|
+
|
|
13
|
+
// A producer has designed a layout over version 3's descriptor: store it beside that version.
|
|
14
|
+
const version = await client.getMethodVersion("mt_abc123", 3);
|
|
15
|
+
// The producer model's JSON patch lines, carried as emitted.
|
|
16
|
+
const jsonl = '{"op":"add","path":"/root","value":"page"}';
|
|
17
|
+
// One key per layout to store, made once and sent again unchanged on each retry of this write.
|
|
18
|
+
const layoutKey = crypto.randomUUID();
|
|
19
|
+
await client.storeMethodLayout(
|
|
20
|
+
"mt_abc123",
|
|
21
|
+
3,
|
|
22
|
+
{
|
|
23
|
+
tree_digest: version.tree_digest!, // the tree the layout was designed over
|
|
24
|
+
pipe_ref: "receipts.review_receipt",
|
|
25
|
+
catalog: "composer",
|
|
26
|
+
prompt_hash: "a1b2c3d4e5f6",
|
|
27
|
+
jsonl,
|
|
28
|
+
provenance: { producer: "pipelex-method", model: "claude-4.8-opus", style: "editorial" },
|
|
29
|
+
},
|
|
30
|
+
{ idempotencyKey: layoutKey },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// A host reads the descriptor and the layouts over it in one call.
|
|
34
|
+
const report = await client.validate({ method_id: "mt_abc123@3" }, false, {
|
|
35
|
+
views: ["input_form", "layouts"],
|
|
36
|
+
});
|
|
37
|
+
if (report.is_valid) {
|
|
38
|
+
const layout: MethodLayout | undefined = report.layouts?.find(
|
|
39
|
+
(candidate) =>
|
|
40
|
+
candidate.pipe_ref === "receipts.review_receipt" && candidate.catalog === "composer",
|
|
41
|
+
);
|
|
42
|
+
// No layout, or none the host can render: the plain form over `report.input_form`.
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The layout
|
|
47
|
+
|
|
48
|
+
| Member | Meaning |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `tree_digest` | The digest of the method tree the layout was produced against, the MTHDS standard's `sha256:` digest of the tree's listing: `sha256:` and 64 lowercase hexadecimal characters. |
|
|
51
|
+
| `pipe_ref` | The namespaced pipe reference it lays out, `domain_path.pipe_code`, as the input-form descriptor keys its forms. |
|
|
52
|
+
| `catalog` | The id of the catalog whose vocabulary it is written in, a slug such as `composer`. |
|
|
53
|
+
| `prompt_hash` | The hash of the prompt the producer's model read, 12 to 64 lowercase hexadecimal digits, so a host recognises a layout designed under an older recipe. |
|
|
54
|
+
| `jsonl` | The layout itself, as the producer's model emitted it: JSON patch lines, one per line, carried as one string exactly as written. |
|
|
55
|
+
| `provenance` | How it was made: `producer`, `model`, `seed`, `style` and `repair_rounds` (below). |
|
|
56
|
+
| `stored_at` | The moment the write landed, ISO 8601 in UTC. The one member the platform sets, absent from the write. |
|
|
57
|
+
| `sample_json` | A written sample of the result the layout was designed over, one JSON text, or `null` when none was stored (see [The written sample](#the-written-sample)). |
|
|
58
|
+
|
|
59
|
+
| `provenance` member | Meaning |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `producer` | Who produced the layout, a slug of the same shape as `catalog`, such as `pipelex-method`. |
|
|
62
|
+
| `model` | The id of the model the producer ran, such as `claude-4.8-opus`. |
|
|
63
|
+
| `seed` | The creative seed handed to the model with the brief, verbatim, or `null` when none was. |
|
|
64
|
+
| `style` | The id of the app style the layout was designed under, such as `editorial`, or `null` when it was designed under none. The platform holds it to the shape `^[a-z0-9-]{1,32}$` and never to a list of styles, so a host reads an id it does not know as no style. |
|
|
65
|
+
| `repair_rounds` | How many repair rounds the producer ran before the layout validated against its catalog. |
|
|
66
|
+
|
|
67
|
+
**A layout belongs to the content it was designed for, never to a version number.** Its identity is `tree_digest`, `pipe_ref`, `catalog` and `prompt_hash` within one method, and it names no method and no version. It is written against a published version, and served for whatever selector of that method resolves the same tree: the version it was written against, a later version that holds the same tree, or the draft while it holds it. A publish that writes a new version also carries the previous version's layouts forward, each copied under the new tree as it stood, so a design follows the method; a host's checks decide whether one still fits. The same tree saved as another method carries none of it, and a copy of a method takes no layout.
|
|
68
|
+
|
|
69
|
+
## Storing a layout
|
|
70
|
+
|
|
71
|
+
| Method | Route | Answer |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `storeMethodLayout(methodId, version, layout, options?)` | `POST /v1/methods/{id}/versions/{n}/layouts` | The `MethodLayout` as stored, with its `stored_at`. |
|
|
74
|
+
|
|
75
|
+
It takes a bare catalog id, as every method route does, and refuses a suffixed one (`mt_…@3`) with a `RequestArgumentError` before anything is sent; `version` is the number of the published version the layout was designed for, and a value that is not a positive integer is refused the same way.
|
|
76
|
+
|
|
77
|
+
**The body is closed at both levels.** The client sends `tree_digest`, `pipe_ref`, `catalog`, `prompt_hash`, `jsonl` and `provenance` with its `producer` and `model`, and `sample_json` and the provenance's `seed`, `style` and `repair_rounds` when each is given and not `null`, and nothing else. A layout read back can be passed as it is: its `stored_at` is left out. A layout or a provenance that is not an object is refused with a `RequestArgumentError` before any request; every other member is sent as given, and the platform holds it to its shape.
|
|
78
|
+
|
|
79
|
+
**`tree_digest` must be the tree the version holds**, its `MethodVersionSummary.tree_digest`, which `getMethodVersion` and `listMethodVersions` read: a producer designs over the descriptor of the version it was handed, so a layout stating another tree describes content the version does not hold, and is refused.
|
|
80
|
+
|
|
81
|
+
**A write of the same identity replaces the layout**, its sample included, so a write that sends no `sample_json` stores none. The answer is the same `200` whether the write stored a new layout or replaced one, and the replaced layout is carried no more.
|
|
82
|
+
|
|
83
|
+
**A write is sent again under its idempotency key.** `options.idempotencyKey` (`MethodLayoutWriteOptions`) is sent as the `Idempotency-Key` header, and nothing is sent when it is absent or `null`. Under the same key the hosted API answers a retry with the first write's stored answer and runs nothing, so a write whose answer was lost, on a client timeout for instance, is sent again unchanged under the key it was first sent with. Without a key the write runs again: it stamps the layout with a new `stored_at`, which moves it ahead of every layout stored since in the newest-first order, and applies the bound below again, so when another producer stored a newer prompt of the same pipe and catalog meanwhile, the resent layout can come back ahead of it and, at the bound, that newer layout can be the one removed. Generate one key per layout to store, a `crypto.randomUUID()` for instance. The write's `200` and its `4xx` refusals are stored under the key alike; a `5xx` and a refusal for its timing (a `408`, `425` or `429`) are not, so the same key sends the write afresh, and neither are the key's own `409` answers below, so a retry after `request.idempotency_key_in_use` runs as the key's first request would. A key is 1 to 255 visible ASCII characters, and anything else is a `RequestArgumentError` before any request.
|
|
84
|
+
|
|
85
|
+
**A content keeps at most 16 layouts**, every pipe, catalog and prompt hash together, which is also the most a validate carries. Past the bound, a write removes first the layouts that newer prompt hashes of their own pipe and catalog superseded, the most superseded and then the oldest first, and the last layout of a pipe and catalog leaves only once more than 16 pipes and catalogs hold layouts. A host laying out several pipes, each in a composer and a result catalog, keeps every one's current layout while another takes new prompts.
|
|
86
|
+
|
|
87
|
+
**Who may write.** The write changes the method, so it takes the roles that change methods: an `admin` or a `member` of the method's organization. A `runner` is refused.
|
|
88
|
+
|
|
89
|
+
## The written sample
|
|
90
|
+
|
|
91
|
+
A designed result page needs something to draw before anyone has run the method, so a layout may carry a sample of its result: `sample_json`, one JSON text of at most 64 KiB of UTF-8, which the platform stores and never reads, refusing it on write when it does not parse. **Only a written sample belongs here, invented from the description of the result and holding no customer's data, never a run's output.** A layout's readers include the method's runners and, for a public method, other organizations, and since the platform never reads the sample, keeping that rule is the producer's. The client reads a `sample_json` that is missing or not a string as `null`, and hands the text back as it is: a host parses it and checks it itself.
|
|
92
|
+
|
|
93
|
+
## The refusals
|
|
94
|
+
|
|
95
|
+
Each is an `ApiResponseError` carrying the platform's problem, and none stores anything. Branch on its `code`, never on the status.
|
|
96
|
+
|
|
97
|
+
| Status | `code` | When |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `422` | `request.invalid` | A member breaking its shape or bound, named in the problem's `params.field` and `location.field`, `provenance.<member>` for one of the provenance's: a missing member, a `tree_digest` that is not `sha256:` and 64 lowercase hexadecimal characters, a `pipe_ref` that is not a dotted reference or is over 256 characters, a `catalog` or `producer` that is not a slug, a `prompt_hash` that is not 12 to 64 lowercase hexadecimal digits, an empty `jsonl`, one over 128 KiB of UTF-8 or one holding a raw control character other than the tab, the line feed and the carriage return, a `model` that is empty, over 128 characters or holds a control character, a `seed` over 4,000 characters, a `style` off its shape, a `repair_rounds` outside 0 to 100, a `sample_json` over 64 KiB of UTF-8 or that is no JSON text, and a lone UTF-16 surrogate in `jsonl`, `model`, `seed` or `sample_json`. |
|
|
100
|
+
| `422` | `request.invalid` naming `tree_digest` | `tree_digest` is not the digest of the tree the version holds. |
|
|
101
|
+
| `403` | `auth.forbidden` | A `runner`, or a caller with no recognised role; its `params.requirement` is `member`. |
|
|
102
|
+
| `404` | `pipelex.catalog.not_found` | No such method in the caller's organization, another organization's included, its `params.resource` `method`; or a version the method never published, its `params.resource` `version`. |
|
|
103
|
+
| `409` | `pipelex.catalog.being_deleted` | The method's erasure is pending, under way or stopped. |
|
|
104
|
+
| `409` | `request.idempotency_conflict` | The idempotency key was sent before with a different body; its `retry` is `no`. |
|
|
105
|
+
| `409` | `request.idempotency_key_in_use` | The key's earlier request is still being handled; its `retry` is `yes`, under the same key in a few seconds. |
|
|
106
|
+
| `500` | `runtime.unexpected` | The version was stored before methods were trees and names no tree, so it takes no layout. |
|
|
107
|
+
|
|
108
|
+
An answer that does not read as a layout, one whose `tree_digest`, `pipe_ref`, `catalog`, `prompt_hash`, `jsonl` or `stored_at` is not a string, or whose `provenance` is not an object with a string `producer` and `model`, is an answer the SDK cannot read, thrown as an `ApiResponseError` coded `client.response_unreadable` whose message names the member.
|
|
109
|
+
|
|
110
|
+
## Reading a version's layouts
|
|
111
|
+
|
|
112
|
+
| Method | Route | Answer |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `listMethodLayouts(methodId, version, options?)` | `GET /v1/methods/{id}/versions/{n}/layouts` | A `MethodLayoutList`, whose `items` are the layouts stored for the version's content, newest first, at most 16, empty when none is. |
|
|
115
|
+
|
|
116
|
+
This is what a host calls when it opens a method's page. It touches only the platform's database and calls no runner, where the `layouts` view below dry-runs every pipe of the method to return the same list. It takes a bare catalog id and a positive integer version, each refused with a `RequestArgumentError` before anything is sent, and `options.signal` cancels the read, for a page that can do without its layouts. A `runner` reads what it is shown, another organization's public method answers the layouts its builders stored, and the answer ignores `layouts_off`, which is the host's to honour.
|
|
117
|
+
|
|
118
|
+
Its refusals are `ApiResponseError`s: `404` `pipelex.catalog.not_found` for an unknown method, another organization's internal one (`params.resource` `method`) and a version never published (`params.resource` `version`); `409` `pipelex.catalog.being_deleted` during the erasure of the caller's own method; `500` `runtime.unexpected` for a version stored before methods were trees. `items` is read entry by entry, by the rule below, so an entry that does not read as a layout is left out; an answer whose `items` is not an array is one the SDK cannot read.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const { items } = await client.listMethodLayouts("mt_abc123", 3, {
|
|
122
|
+
signal: AbortSignal.timeout(5000),
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Switching a page's design off
|
|
127
|
+
|
|
128
|
+
A builder can turn a page's design off and on, per catalog, without losing it. The state is `layouts_off`, the catalog slugs whose layouts no host draws, read on `MethodData`, `MethodDetail` and `PublicMethodDetail` and absent from a platform that predates it, which means none.
|
|
129
|
+
|
|
130
|
+
| Method | Route | Answer |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `setMethodLayoutsOff(methodId, catalogs, options?)` | `PUT /v1/methods/{id}/layouts-off` | The method, its `layouts_off` the list as held. |
|
|
133
|
+
|
|
134
|
+
`catalogs` is the whole list and replaces the one held, so changing one catalog means reading `layouts_off` and writing the new list; an empty list switches every design on. The switch acts at once with no publish and holds across every publish, and a layout switched off is kept: `listMethodLayouts` and the `layouts` view still answer it, and the host draws the plain page. Like a rename, the write never moves the draft token, so the `updated_at` a caller holds stays valid.
|
|
135
|
+
|
|
136
|
+
**The last write to land is the one held.** A write sent again without a key writes its list again, and so undoes a switch that landed in between. `options.idempotencyKey` (`MethodLayoutsOffOptions`) is sent as the `Idempotency-Key` header, so a write whose answer was lost is sent again under its key and answered as the first was, by the rule [a layout write](#storing-a-layout) follows.
|
|
137
|
+
|
|
138
|
+
A `catalogs` that is not an array is a `RequestArgumentError` before any request, and its entries are sent as given. The route's body is closed, and the platform answers: `422` `request.invalid` for more than 8 catalogs, a catalog named twice or one that is not a slug; `403` `auth.forbidden` for a `runner`; `404` `pipelex.catalog.not_found` for an unknown method or another organization's, a public one included; `409` `pipelex.catalog.being_deleted` during the erasure; and `413` `request.too_large` for a list that would leave the method too large to publish, which a list that does not grow never is, so switching a design back on is always reachable.
|
|
139
|
+
|
|
140
|
+
## Reading the layouts: the `layouts` view
|
|
141
|
+
|
|
142
|
+
A host that is already validating the method can read its layouts on the same call, at the cost of the validate's dry run. A `validate` naming a stored method by `method_id` (`mt_…`, `mt_…@<n>` or `mt_…@draft`) and naming `layouts` among its `views` is answered, on the valid report, with `layouts`: every layout stored for the tree the selector resolved, newest first by `stored_at`, at most 16. Ask for `views: ["input_form", "layouts"]` to receive the descriptor and the layouts over it in one answer. `ValidateView` types the tokens this SDK knows, `input_form`, `output_form` and `layouts`, and `ValidateOptions.views` still takes any other token as its raw string.
|
|
143
|
+
|
|
144
|
+
The token rides the `views` list with that list's mechanics: the client sends it verbatim and only when asked, a runner that does not know it ignores it, and it never causes a `422`. The platform forwards `views` as sent and adds the member to the runner's answer.
|
|
145
|
+
|
|
146
|
+
**Absent means none.** `PipelexValidationReport.layouts` is optional, and the platform leaves it out, so a host renders the plain form, whenever:
|
|
147
|
+
|
|
148
|
+
- the validate names no `method_id`, since a method sent as its tree has no stored layouts, whatever its tree;
|
|
149
|
+
- `views` does not name `layouts`;
|
|
150
|
+
- the verdict is invalid, since a layout is rendered over the descriptor and the descriptor rides the valid report alone;
|
|
151
|
+
- no layout is stored for the resolved tree;
|
|
152
|
+
- the platform could not read the stored layouts, which costs the validate nothing else.
|
|
153
|
+
|
|
154
|
+
A host therefore never reads an empty array here. Publishing a version with another tree leaves the layouts of the old tree where they were, and the old version named by its number still carries them all; the new tree carries the copies the publish made of them. Another organization's public method carries the layouts its holder's builders stored.
|
|
155
|
+
|
|
156
|
+
**The client reads `layouts` entry by entry**, as it reads a verdict's problem items, so one layout it cannot read never costs the caller the verdict or the other layouts: an entry that does not read as a layout, by the rule the write's answer is held to, is left out, and a list left with none is removed. In each entry, a `sample_json` or a provenance `seed` or `style` that is missing or not a string reads as `null`, and a `repair_rounds` that is missing or not a non-negative integer reads as `0`, the platform's own defaults; the write's answer and the direct read's entries are read the same way.
|
|
157
|
+
|
|
158
|
+
**Discovery.** `GET /v1/version` lists `method_layouts` among its `extensions` once the platform stores layouts and carries the view. Without it, a validate answering no `layouts` cannot tell a method nobody laid out from a platform that ignores the token, so a host that does not find it offers no layout to produce, since the write would be refused, and renders the plain form. The SDK types no member for it: read it off `version()`, whose answer keeps every extension member as the server sent it.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const info = await client.version();
|
|
162
|
+
const storesLayouts =
|
|
163
|
+
Array.isArray(info.extensions) && info.extensions.includes("method_layouts");
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## What a host does with a layout
|
|
167
|
+
|
|
168
|
+
A host renders a layout only when it can, and renders the plain form over the descriptor otherwise. It sets a layout aside when it does not render its `catalog`, when it no longer accepts its `prompt_hash`, when the layout's `pipe_ref` is not the pipe it is showing, or when the layout no longer fits the descriptor, such as a binding to a path the descriptor does not hold. Among the layouts it can render it picks one by its own rule, which may read `provenance.style` to prefer the layout designed under the style it shows. A host never sends a layout back, and never fails the page for want of one.
|