@pipelex/sdk 0.46.0 → 0.47.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 +13 -4
- package/dist/cli/script.d.ts +1 -1
- package/dist/client.d.ts +168 -61
- package/dist/client.js +360 -120
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +45 -18
- package/dist/errors.js +46 -20
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/method-trees.d.ts +209 -0
- package/dist/method-trees.js +689 -0
- package/dist/method-trees.js.map +1 -0
- package/dist/product-models.d.ts +97 -73
- package/dist/runs.d.ts +5 -3
- package/dist/runs.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +5 -5
- package/docs/bundle-resources.md +1 -1
- package/docs/crate-routes.md +2 -2
- package/docs/errors.md +3 -2
- package/docs/input-preparation.md +5 -30
- package/docs/method-trees.md +101 -0
- package/package.json +2 -2
- package/dist/method-source.d.ts +0 -19
- package/dist/method-source.js +0 -50
- package/dist/method-source.js.map +0 -1
package/docs/errors.md
CHANGED
|
@@ -51,9 +51,10 @@ On an error from a request that may have started a run, `retryable` is `false` w
|
|
|
51
51
|
| `RunLifecycleUnavailableError` | `config` | no | The base URL points at a bare runner without the run lifecycle. |
|
|
52
52
|
| `MissingMainStuffError` | `runtime` | no | The API broke its own contract on a completed run. |
|
|
53
53
|
| `PagingNotTerminatingError` | `runtime` | no | A server minting cursors forever; `pageLimit` is the ceiling `iterateMethods`, `iterateRuns` or `iterateMethodExamples` stopped at. |
|
|
54
|
-
| `InputPreparationError` and `
|
|
54
|
+
| `InputPreparationError` and `InvalidLocalSourceError`, `InvalidInputValueError`, `UploadNotAllowedError`, `MethodLoadError`, `RejectedAssetError` | `input` | no | The asset, the inputs or the method selector must change. The family's base takes an optional `verdict` in its options, so a subclass of your own can declare another; `prepareInputs` declares `runtime` for a pipe I/O answer that breaks the route's contract: no boolean `is_valid`, an `input_form` that does not describe the pipe it selected, or a descriptor that is malformed or names a kind this SDK does not know. |
|
|
55
55
|
| `UnsupportedUploadCapabilityError`, `UploadAuthenticationError` | `config` | no | The deployment or the credential must change. |
|
|
56
56
|
| `UploadTransportError` | the cause's, else by `code` | the cause's, else by `code` | `uploadFile` wraps the `ApiResponseError` or `ApiUnreachableError` the client threw, and takes its verdict, so a wrapped `402` is `config` and not retryable. Without such a cause, which is always the case from `uploadWithGrant`: `timeout`, `storage_timeout` and `conflict` are `runtime` and retryable, `conflict` because storage documents its `409 ConditionalRequestConflict` as retryable and the grant is not spent by it (a spent grant is the `412` of a `RejectedAssetError` with code `grant_used`); `server_error` is `runtime`, and retryable when [the fallback table](#a-refused-requests-verdict) would call its `status` retryable, any `5xx` but the `501` storage answers for a request it does not implement, or when it carries no status; `unexpected` is `runtime`, and retryable when that table would call its `status` retryable, which is the `408` or `429` storage answers when it times out or throttles the request rather than refusing the file, and not retryable for any other status or none; `unreachable` is `config` and retryable; `redirected` is `config` and not retryable; `invalid_grant_url` is `runtime` and not retryable. |
|
|
57
|
+
| `MethodFileTransferError` | `runtime` | by `code` | A method's file could not be moved between this machine and storage: uploaded by `createMethod` or `writeDraft`, or downloaded by `writeMethodTree`. `unreachable` (no answer), `storage_error` (`408`, `429` or a `5xx` other than `501`) and `mismatch` (downloaded bytes that are not the file listed) are retryable; `refused`, any other status, an expired link among them, is not. It carries `operation`, `code`, `sha256`, `path` and `status`, and neither the link, nor storage's error body, nor a cause ([method-trees.md](./method-trees.md#when-a-transfer-fails)). |
|
|
57
58
|
| `ArtifactOperationError`, `ScopeUnavailableError` | `runtime` | no | The family's base also takes an optional `verdict`: the SDK declares `input` where it refuses an argument (a `scope`, a bound, a location) and `config` where the environment refuses the download (a runtime with no filesystem, a directory that cannot be created). |
|
|
58
59
|
| `ArtifactAuthenticationError` | `config` | no | The credential must change. Its `verdict` member is the download's result so far, not the error's. |
|
|
59
60
|
| `ArtifactFetchError` | by `code` | by `code` | `invalid_storage_uri`, `forbidden`, `unsupported_url`, `not_found` and `too_large` are `input` and not retryable; `plain_http_refused` is `config` and not retryable; `redirect_refused` and `store_refused` are `runtime` and not retryable; `timeout` and `network` are `runtime` and retryable; `store_error` is `runtime`, and retryable when [the fallback table](#a-refused-requests-verdict) would call its `status` retryable: a `408`, a `429`, or a `5xx` other than `501`. |
|
|
@@ -259,7 +260,7 @@ So `caller_facing_message` is how a consumer tells the author's copy from the pl
|
|
|
259
260
|
|
|
260
261
|
Every non-2xx answer from a `/v1` route is a problem document, and the SDK throws an `ApiResponseError` whose fields are its members. Each field is `undefined` when the document did not carry it, and a member of the wrong type (a numeric `type`, a `retryable` of `"no"`, a `user_action` without a `detail`) reads as absent rather than as a wrong value. The nested members are checked one level down, exactly as a [stored report's](#a-failed-run--runerrorreport) are: `providerMetadata` and `migration` keep each named field only when it has its type, `validationErrors` keeps only the items that are objects with a string `category` and `message`, and each item of `errors` keeps its `field`, `code` and `detail` only when they are strings; members the SDK does not name are relayed as sent. The verdict is the exception: `errorDomain` and `retryable` are always decided, the server's value when it sent a valid one and otherwise [as the verdict section says](#a-refused-requests-verdict).
|
|
261
262
|
|
|
262
|
-
**An answer the SDK could not read is an `ApiResponseError` too.** A `2xx` whose body is not JSON, such as a gateway's HTML page or an empty body where the route answers one, is no result the SDK can hand back, so every route throws an `ApiResponseError` for it instead of letting the runtime's `SyntaxError` escape. Every body is read by the rules the protocol reads a problem body by (`parseJsonBody` in `mthds/protocol`, as `pipelex-sdk` reads it), so JSON nested deeper than `MAX_BODY_NESTING`, or holding a number past the range of a double, such as `1e999` or an integer that long, which `JSON.parse` reads as `Infinity`, is a body that is not JSON too; a refusal whose body is such JSON carries no hosted member and no `problemDocument`. So is a `2xx` read exactly, such as a run's results or a method example, that holds a number past `Number.MAX_SAFE_INTEGER` on a runtime whose `JSON.parse` hands a reviver no source text, which could only read it rounded; its `cause` is the reader's `ExactJsonUnsupportedError` ([exact-integers.md](./exact-integers.md)). So is a `2xx` whose body is JSON but not the object the route answers — `null`, a number, a string, an array — on every protocol, extension and run-lifecycle route, on `health()`, and on the product routes whose answer the SDK or its caller reads before anything else (`listMethods`, `getMethod`, `createMethod`, `writeDraft`, `renameMethod`, `publishMethod`, `listMethodVersions`, `getMethodVersion`, the method example routes, `listRuns`, `getRunDetail`, `upload`, `requestUploadGrant`, `resolveStorageUrls`), instead of the `TypeError` that reading it would raise. So is an object missing the member those reads depend on: a page of `listMethods`, `listMethodVersions`, `listMethodExamples` or `listRuns` whose `items` is not an array or whose `next_cursor` is neither a string nor `null`, which the iterators could not walk or would follow forever; a publish result whose `outcome` is not `published`, `unchanged` or `refused`, or whose `method` is not an object, and a latest-run result whose `outcome` is not `recorded` or `stale`, or whose `example` is not an object, which a caller branching on `outcome` could not read; a method example, as `createMethodExample`, `getMethodExample` and `updateMethodExample` answer it, as a row of `listMethodExamples` and as a latest-run result's `example`, that misses a field the Python SDK's model of an example requires or holds one of another type, its message naming the field; an `upload` answer with no non-empty string `uri`, which names no stored file;
|
|
263
|
+
**An answer the SDK could not read is an `ApiResponseError` too.** A `2xx` whose body is not JSON, such as a gateway's HTML page or an empty body where the route answers one, is no result the SDK can hand back, so every route throws an `ApiResponseError` for it instead of letting the runtime's `SyntaxError` escape. Every body is read by the rules the protocol reads a problem body by (`parseJsonBody` in `mthds/protocol`, as `pipelex-sdk` reads it), so JSON nested deeper than `MAX_BODY_NESTING`, or holding a number past the range of a double, such as `1e999` or an integer that long, which `JSON.parse` reads as `Infinity`, is a body that is not JSON too; a refusal whose body is such JSON carries no hosted member and no `problemDocument`. So is a `2xx` read exactly, such as a run's results or a method example, that holds a number past `Number.MAX_SAFE_INTEGER` on a runtime whose `JSON.parse` hands a reviver no source text, which could only read it rounded; its `cause` is the reader's `ExactJsonUnsupportedError` ([exact-integers.md](./exact-integers.md)). So is a `2xx` whose body is JSON but not the object the route answers — `null`, a number, a string, an array — on every protocol, extension and run-lifecycle route, on `health()`, and on the product routes whose answer the SDK or its caller reads before anything else (`listMethods`, `getMethod`, `createMethod`, `writeDraft`, `renameMethod`, `publishMethod`, `listMethodVersions`, `getMethodVersion`, the method example routes, `listRuns`, `getRunDetail`, `upload`, `requestUploadGrant`, `resolveStorageUrls`), instead of the `TypeError` that reading it would raise. So is an object missing the member those reads depend on: a page of `listMethods`, `listMethodVersions`, `listMethodExamples` or `listRuns` whose `items` is not an array or whose `next_cursor` is neither a string nor `null`, which the iterators could not walk or would follow forever; a publish result whose `outcome` is not `published`, `unchanged` or `refused`, or whose `method` is not an object, and a latest-run result whose `outcome` is not `recorded` or `stale`, or whose `example` is not an object, which a caller branching on `outcome` could not read; a method example, as `createMethodExample`, `getMethodExample` and `updateMethodExample` answer it, as a row of `listMethodExamples` and as a latest-run result's `example`, that misses a field the Python SDK's model of an example requires or holds one of another type, its message naming the field; an `upload` answer with no non-empty string `uri`, which names no stored file; a stored method or a published version, as `getMethod` and `getMethodVersion` answer it, whose `files` is not a listing of files, each with its string `path`, `url` and `expires_at`, a `sha256` of 64 lower-case hex digits and a non-negative integer `size`; and an answer of `POST /v1/blobs/missing` asking for a blob the tree being saved does not hold: that is server data no change to the call fixes, not an argument the caller passed. Its `status` is the `2xx`, `responseBody` the raw text, `cause` what made the answer unreadable (the parse failure; nothing for a body that parsed to something the route does not answer, nor for JSON the protocol refuses), `problemDocument` and the hosted problem members are `undefined`, `problem` is the standard's reading of such an answer (`client.response_unreadable`, or `client.outcome_unknown` on `start` and `execute`), an answer that carries signed links (a stored method, a published version, the upload targets of `POST /v1/blobs/missing`) has every link redacted in `responseBody`, each `url` member and each link's query replaced by `[redacted]`, and keeps no `cause` at all, since a link is a bearer credential; and its verdict is the fallback's for a status no refusal carries: `runtime`, not retryable. On `start` and `execute` its `runMayHaveStarted` is `true`, since the server accepted the request; that includes a start acknowledgement whose `pipeline_run_id` is not a string, and an `execute` `202` that names no run, which leave the caller a run it could not follow, and an `execute` answer that is not the completed run: one whose `pipeline_run_id` is not a string, or whose `pipe_output` carries no string `pipeline_run_id` or no `working_memory` with `root` and `aliases` objects, the members the Python SDK's model of the answer requires. A product route that answers a `2xx` with no body at all, such as `revokePipelexApiKey`, still resolves to `undefined`, and the other product routes hand their decoded body back as it came.
|
|
263
264
|
|
|
264
265
|
| Field | Wire member | Meaning |
|
|
265
266
|
|---|---|---|
|
|
@@ -119,7 +119,7 @@ Exactly one per call. The type pins the other two to `never`, so a second select
|
|
|
119
119
|
|
|
120
120
|
Inline `files` may bring `resources`, the files the closure's steps read, such as a `PipeDocGen` step's `template_file`, keyed by the path the step names, each as its bytes ([`bundle-resources.md`](./bundle-resources.md)). They ride the one pipe I/O request, which loads the closure and fails to load it, as a `MethodLoadError`, when a step's template file was not sent. A published or stored method's files are its own, so `resources` beside `method_ref` or `method_id` is an `InputPreparationError` before any request.
|
|
121
121
|
|
|
122
|
-
Nothing is expanded client-side: `method_id` here is a **pass-through**, the same rule every other id-taking operation follows
|
|
122
|
+
Nothing is expanded client-side: `method_id` here is a **pass-through**, the same rule every other id-taking operation follows (see "[A method's files in hand](#a-methods-files-in-hand)").
|
|
123
123
|
|
|
124
124
|
#### Where the signature comes from
|
|
125
125
|
|
|
@@ -131,7 +131,7 @@ pipeIo({ <files [+ resources] | method_ref | method_id>, pipe_ref? })
|
|
|
131
131
|
|
|
132
132
|
The route resolves the closure, **selects the pipe**, and answers with that pipe's **input-form descriptor** under the qualified ref it resolved. It runs no dry run, so preparing inputs costs one static load rather than a validation of the whole method. It also refuses nothing for a pending signature elsewhere in the method: preparation needs a pipe's *declared* inputs, and whether the method runs is the run's verdict, not preparation's. An `is_valid: false` verdict still means the closure does not load, which is a preparation failure.
|
|
133
133
|
|
|
134
|
-
A `method_ref` makes the server clone a repository first, so the call gets the crate routes' fetch-sized budget, and `resources` beside inline `files` get the
|
|
134
|
+
A `method_ref` makes the server clone a repository first, so the call gets the crate routes' fetch-sized budget, and `resources` beside inline `files` get the client's request ceiling (`requestTimeoutMs`, twenty minutes by default), since uploading them can take longer than the default on its own. On the hosted API the gateway caps a request at about 30 seconds whatever the client allows, so a cold clone can answer a `502` `ApiResponseError`; a retry clears it once the runner has cached the clone.
|
|
135
135
|
|
|
136
136
|
`prepareInputs` needs an API that serves `POST /v1/pipe-io`. Against one that does not, the route's `404` (a runner) or `403` (a hosted origin whose gateway does not list it yet) propagates as an `ApiResponseError`. It also needs the route's pipe-selection refusals typed, as step 3 of "Pipe selection" below describes, which `pipelex-api` does from v0.33.1: v0.33.0, the release that added the route, typed them as the generic `ValidationError`, so against it a refused selection reaches the caller as that `ApiResponseError` rather than an `InputPreparationError`.
|
|
137
137
|
|
|
@@ -160,35 +160,11 @@ client.prepareInputs({ method_id, inputs, allowUploads: false }) → PreparedInp
|
|
|
160
160
|
|
|
161
161
|
The error names the input as `inputName` and where inside it the value sits as `inputPath`: the input's name for a top-level file, then `.field` for each declared field and `[index]` for each list element (`dossier.attachments[1]`). An explicit envelope adds nothing to the path, since its `content` is the input's value. `valueKind` says what the value is, `local_path`, `data_url` or `bytes`, so a consumer can tell its caller what to send instead; the value itself is never in the message or on the error, since it is the caller's data. The option is `true` when omitted, must be a boolean when given — anything else is refused with an `InputPreparationError` before any request, since reading a stray `"false"` as permission would upload exactly what the caller meant to keep — and is not sent to the server. The option covers `prepareInputs` alone: `uploadFile` is an upload by definition.
|
|
162
162
|
|
|
163
|
-
##
|
|
163
|
+
## A method's files in hand
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
client.getMethodClosure(methodId) → MthdsFileItem[]
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
`getMethodClosure` is the public **local expansion utility** — a client-side semantic layer over `getMethod` (the platform has no route that returns a parsed closure). It fetches the method, parses its **draft's** polymorphic `mthds` source with [`methodSourceToContents`](#methodsourcetocontents--the-canonical-source-parser), and labels each resulting file with the `method_id` as its `source` provenance.
|
|
170
|
-
|
|
171
|
-
**It reads the draft, which is not what a bare id runs.** `GET /v1/methods/{id}` answers the method with its draft, whatever the publish state, while a run or a tooling call by a bare `method_id` resolves to the latest *published* version, which the draft may be ahead of. For the closure of a version, read it with `getMethodVersion(methodId, n)` and parse its `mthds` with `methodSourceToContents`. Like `getMethod`, it takes a bare id: a caller holding `mt_…@3` strips the suffix with `parseMethodSelector` first.
|
|
172
|
-
|
|
173
|
-
Reach for it whenever you want the files **in hand** — to edit, to diff, or to send to a bare runner, which has no catalog to resolve an id against:
|
|
174
|
-
|
|
175
|
-
```
|
|
176
|
-
runner.resolve({ files: await hosted.getMethodClosure("mt_…") })
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Every method-taking operation — `execute` / `start`, `validate` / `resolve` / `codegen` / `pipeIo`, and `prepareInputs` — takes a `method_id` **natively** on the hosted API, as a server pass-through, its version suffix included (`mt_…`, `mt_…@<n>`, `mt_…@draft`): the platform resolves it and injects the source of the version it names before the runner sees the request (see [architecture.md → hosted run extensions](./architecture.md#hosted-run-extensions-method_id)). That is the uniform rule — *an id option is always a server pass-through, and any client-side expansion is the caller's own explicit call*.
|
|
180
|
-
|
|
181
|
-
- **Requires an API key.** The methods catalog is org-scoped to the key's org, so resolution only works with an authenticated Pipelex-product key.
|
|
182
|
-
- **Unknown or foreign-org id → the `getMethod` `404`** (`ApiResponseError`, `code: "not_found"`), propagated unchanged. An id from another org is indistinguishable from a nonexistent one — both 404.
|
|
183
|
-
- **A real, in-org method whose source parses to nothing → `EmptyMethodSourceError`** — the row exists but has no runnable MTHDS source yet. This is a distinct outcome from the 404 (see "[Error and capability behavior](#error-and-capability-behavior)"), and `getMethodClosure` is now its only raiser: `prepareInputs` passes the id to the server, which answers a sourceless method with a `422`.
|
|
184
|
-
|
|
185
|
-
### `methodSourceToContents` — the canonical source parser
|
|
186
|
-
|
|
187
|
-
```
|
|
188
|
-
methodSourceToContents(mthds: string) → string[]
|
|
189
|
-
```
|
|
165
|
+
Every method-taking operation — `execute` / `start`, `validate` / `resolve` / `codegen` / `pipeIo`, and `prepareInputs` — takes a `method_id` **natively** on the hosted API, as a server pass-through, its version suffix included (`mt_…`, `mt_…@<n>`, `mt_…@draft`): the platform resolves it and injects the tree of the version it names before the runner sees the request (see [architecture.md → hosted run extensions](./architecture.md#hosted-run-extensions-method_id)). That is the uniform rule — *an id option is always a server pass-through, and any client-side expansion is the caller's own explicit call*.
|
|
190
166
|
|
|
191
|
-
A
|
|
167
|
+
A caller that wants a method's files in hand, to edit, to diff, or to send to a bare runner, which has no catalog to resolve an id against, reads them explicitly. `getMethod(id)` lists the draft's tree and `getMethodVersion(id, n)` a published version's, each file with a short-lived link to its bytes, and `writeMethodTree(files, { destination })` writes that listing to disk, checked against the listing ([`method-trees.md`](./method-trees.md)). The draft is not what a bare id runs: a run or a tooling call by a bare `method_id` resolves to the latest published version, which the draft may be ahead of.
|
|
192
168
|
|
|
193
169
|
## Compact or explicit-envelope inputs
|
|
194
170
|
|
|
@@ -248,7 +224,6 @@ Upload is a **hosted Pipelex-product capability**, even though the SDK can be po
|
|
|
248
224
|
The contract distinguishes these semantic outcomes, each a typed subclass of `InputPreparationError` (catch the base to handle any preparation failure, a subclass to branch on category):
|
|
249
225
|
|
|
250
226
|
- **an unresolvable signature** (`InputPreparationError`) — a bare or alias-qualified `pipe_ref`, refused before any request; a closure that does not load (`MethodLoadError`, below); or a pipe selection the route refused (its `422` typed `EntryPipeNotFoundError` or `EntryPipeAmbiguousError`, carrying the server's detail). Any other failure from `/v1/pipe-io` — a malformed selector, an over-limit file, an unknown or foreign-org `method_id`, no package at the address or one with no `.mthds` file, auth, a server fault, a deployment that does not serve the route — stays an `ApiResponseError` and propagates unchanged;
|
|
251
|
-
- **empty method source** (`EmptyMethodSourceError`, carries `methodId`) — `getMethodClosure` found the stored method but its `mthds` source parses to nothing (the row exists, no runnable source yet). Distinct from the `getMethod` `404` for an unknown/foreign id, which stays an `ApiResponseError`. `prepareInputs` never raises it: it hands the id to the server, which answers a sourceless method with a `422`;
|
|
252
227
|
- **a method that does not load** (`MethodLoadError`) — the pipe I/O answer's `is_valid: false` arm. Its message names the first validation item's message, else the answer's own `message`; `validationErrors` holds the answer's items, each kept when it carries a string `category` and `message`, and `serverMessage` the answer's own `message` when it is a string, so a consumer lists every item rather than reading the first from a sentence. The method must change, not the inputs;
|
|
253
228
|
- **invalid local source** (`InvalidLocalSourceError`) — missing, unreadable, or a path string outside Node;
|
|
254
229
|
- **invalid input value** (`InvalidInputValueError`, carries `inputName` and `inputPath`) — a value at a file input that cannot be turned into a file to upload: a `data:` URL that does not decode (no comma, bad base64, bad percent-encoding), or a value of a type no file input takes, such as a number or an object that is neither bytes nor `{url}` content. It is raised while the inputs are read, before anything is uploaded. It names the input and the path inside it as `UploadNotAllowedError` does (`Input "dossier.attachments[1]" holds a number, which no file input takes: …`), describes the value by its kind and never quotes it, and lists the forms a file input takes in the mode it ran in. With `InvalidLocalSourceError`, it is how a consumer tells the inputs' own mistakes from a method that does not load, though all three are `InputPreparationError`s with the same verdict;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# A saved method's tree
|
|
2
|
+
|
|
3
|
+
A saved method holds a **method tree**: the files a method is made of, each at its tree path, the same tree a run takes as `files`. The draft holds one, and each published version holds the one it was published from. A tree is named by its **tree digest**, `sha256:` and the SHA-256 of its listing, so two methods holding the same files hold the same digest. Both are defined by the MTHDS standard (`mthds/docs/spec/method-tree.md`), and the `mthds` package lists a tree from a directory with `listMethodTree`, whose `MethodTree` a save takes as it is.
|
|
4
|
+
|
|
5
|
+
The platform keeps each file once per organization, as a blob named by its SHA-256, so a save sends only the files the organization does not hold yet, and a read lists the files with a short-lived link to each one's bytes.
|
|
6
|
+
|
|
7
|
+
## Saving a tree
|
|
8
|
+
|
|
9
|
+
`createMethod(input)` and `writeDraft(methodId, input)` take the tree in one of three forms (`MethodTreeInput`):
|
|
10
|
+
|
|
11
|
+
| Form | What it is | How it is sent |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `files` | The standard's map of file entries (`MethodFiles` from `mthds/protocol`): a text, `{ base64 }`, or `{ sha256, size }` for bytes the organization already holds. | As given. |
|
|
14
|
+
| `tree` | A `MethodTreeSource`: a tree's `files` (each with its `path`, `size` and `sha256`), its `digest`, and `readFile(path)` giving each file's bytes. The `MethodTree` `listMethodTree` returns is one. | Inline when small, by hash otherwise (below). |
|
|
15
|
+
| `contents` | Each file's bytes (`Uint8Array`) or its text, by tree path. | Inline when small, by hash otherwise (below). |
|
|
16
|
+
|
|
17
|
+
A tree given as `tree` or `contents` is sent **inline** when its inline map weighs `INLINE_TREE_MAX_BYTES` (256 KiB) or less, written as JSON with no whitespace and measured in UTF-8, or when the platform keeps no blob store, which its version handshake says (`blob_store`). Inline, a file whose bytes are UTF-8 travels as its text and any other as `{ base64 }`. Otherwise the client asks `POST /v1/blobs/missing` which files the organization lacks, naming the tree's digest, uploads each one it lacks once, at most eight at a time, straight to the upload target the platform names, under the client's request time limit (`requestTimeoutMs`, twenty minutes unless the client was built with another), and sends every file by hash. A save carrying a tree, inline or by hash, takes the same request time limit, since a tree sent inline to a platform with no blob store has no size bound. These are the rules the `mthds` package's own clients follow, so the tree a save sends is the tree a run would send. A file uploaded once is never uploaded again, by this method or any other of the organization.
|
|
18
|
+
|
|
19
|
+
A text in `contents` is encoded in UTF-8, and a text a file cannot hold, one carrying a lone surrogate, is refused, as is a value that is neither a string nor a `Uint8Array`. A `MethodTree` reads a file again whenever its bytes are sent, inline or as an upload, and checks it against the hash its listing recorded, so a file changed since the listing is refused by `mthds` rather than sent as another tree than the one listed. Saving reads no Node module of its own: hashing is Web Crypto's.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { listMethodTree } from "mthds";
|
|
23
|
+
|
|
24
|
+
const tree = await listMethodTree("methods/receipts", { limits: await client.methodTreeLimits() });
|
|
25
|
+
const method = await client.createMethod({ name: "Receipts", tree });
|
|
26
|
+
|
|
27
|
+
const draft = await client.writeDraft(method.method_id, {
|
|
28
|
+
contents: { "main.mthds": source, "helpers/fx.py": helper },
|
|
29
|
+
expected_updated_at: method.updated_at,
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**A draft write replaces the whole tree.** `writeDraft` refuses a draft that gives no tree, and `files: null` empties the draft. `createMethod` with no tree creates a method with an empty draft. The client refuses two forms at once, and a `tree` or `contents` holding no file, with a `RequestArgumentError` before anything is sent.
|
|
34
|
+
|
|
35
|
+
**What is uploaded is what the listing names.** Before each upload the client hashes the bytes a tree's `readFile` gives and refuses them, with a `RequestArgumentError`, unless they have the size and the SHA-256 the tree lists for them: a blob is stored under its hash for the whole organization, so a hand-built `MethodTreeSource` whose `readFile` disagrees with its listing never stores other bytes under that hash. An answer of `POST /v1/blobs/missing` asking for a blob the tree does not hold is the platform's fault, not the caller's: it is an `ApiResponseError` of an answer the SDK cannot read, verdict `runtime` and not retryable, thrown before any upload.
|
|
36
|
+
|
|
37
|
+
The save is refused, with the `ApiResponseError` of its problem, when the platform will not take the tree:
|
|
38
|
+
|
|
39
|
+
| Status and `code` | When |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `422` `invalid_method_files` | The map breaks the standard's rules, a path that is not a tree path or two paths that collide among them; the problem's `tree_refusal` names the rule. |
|
|
42
|
+
| `422` `method_tree_too_large` | The tree passes one of the deployment's limits, which the problem's `limit` names. |
|
|
43
|
+
| `422` `blob_mismatch` | A hash entry states another size than the blob the organization holds under that hash. |
|
|
44
|
+
| `422` `missing_blobs` | A hash entry names bytes the organization does not hold. A save built from `tree` or `contents` uploads every blob it names first, so it meets this only if a blob was removed between the upload and the save; the client does not retry it. |
|
|
45
|
+
| `413` `payload_too_large` | The method would leave no room for the summary a publish adds. |
|
|
46
|
+
|
|
47
|
+
An upload that fails throws `MethodFileTransferError` with `operation` `upload`, and the method is not saved (below).
|
|
48
|
+
|
|
49
|
+
## The deployment's limits
|
|
50
|
+
|
|
51
|
+
`methodTreeLimits()` returns the limits the deployment holds a tree to, `{ maxFileCount, maxFileSize, maxTotalSize }`, read from `limits.method_tree` of the public version handshake (`GET /v1/version`), in the shape `listMethodTree` takes as its `limits` option. A limit the server does not announce is absent, a value that is not a positive integer is left out rather than trusted, and none is ever assumed, so a bare runner, which announces none, yields no limit. The client asks for the handshake once and keeps its answer, as it keeps the run lifecycle's availability, so `methodTreeLimits` and every save sent by hash read the same answer; a handshake that fails is asked again on the next call. Listed under those limits, a tree the platform would refuse for its size is refused while it is listed, with `mthds`'s `TreeLimitExceededError` naming the limit, before any file is hashed.
|
|
52
|
+
|
|
53
|
+
## Reading a tree
|
|
54
|
+
|
|
55
|
+
`getMethod(methodId)` answers a `MethodDetail`: the method, with `files`, the draft's tree listed in tree path order. `getMethodVersion(methodId, version)` answers a `MethodVersion`: the version's summary, with the same `files`. Each file is a `SavedMethodFile`:
|
|
56
|
+
|
|
57
|
+
| Field | Meaning |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `path` | Its tree path. |
|
|
60
|
+
| `sha256` | The SHA-256 of its bytes, 64 lower-case hex digits. |
|
|
61
|
+
| `size` | Its size in bytes. |
|
|
62
|
+
| `url` | A link to its bytes, signed by storage, which any HTTP client reads with no credential. |
|
|
63
|
+
| `expires_at` | When the link stops working, about fifteen minutes after the read. |
|
|
64
|
+
|
|
65
|
+
An empty draft lists no file and has a `draft_digest` of `null`. The listing pages are lighter: `listMethods` rows and `listMethodVersions` summaries carry the digest and no files. The writes, `createMethod`, `writeDraft` and `renameMethod`, answer `MethodData`, the method without its files.
|
|
66
|
+
|
|
67
|
+
**A link is a bearer credential.** Anyone holding it reads the file until it expires, so it belongs in no log, message or stored record; this SDK puts it in none of its errors, not even when the answer that carried it cannot be read. A read that fails because a link expired succeeds once the method is read again.
|
|
68
|
+
|
|
69
|
+
A method whose draft names a tree the platform holds no record of answers `getMethod` with a `409` whose verdict is `runtime`, and a version stored before methods were trees and never rewritten as one names no tree to serve, so `getMethodVersion` answers it with `409` `conflict`, whose verdict is `runtime` too. An answer whose `files` is not a listing, each entry with its string `path`, `url` and `expires_at`, a `sha256` of 64 lower-case hex digits and a non-negative integer `size`, is an `ApiResponseError` naming the entry at fault. Such an answer, like any other the SDK cannot read from these two routes or from `POST /v1/blobs/missing`, reaches its error redacted: `responseBody` has every `url` member and every link's query replaced by `[redacted]`, and the error keeps no `cause`, since a parse failure can quote the body.
|
|
70
|
+
|
|
71
|
+
## Writing a tree to disk
|
|
72
|
+
|
|
73
|
+
`writeMethodTree(files, { destination })` writes a listing to disk, each file at its tree path under `destination`. It runs in Node only, and loads Node's file system modules when it is called:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const detail = await client.getMethod("mt_123");
|
|
77
|
+
await client.writeMethodTree(detail.files, { destination: "receipts" });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Each file is downloaded through its link, at most eight at a time, with no credential and no header of this client, following no redirect, under the client's request time limit (`requestTimeoutMs`), which covers the whole download, the body included: a body that stalls once its headers arrived fails with the timeout's code, `ABORT_TIMEOUT`. Each file's size and SHA-256 are checked against the listing, and no more than its listed size and one byte is read, so what lands on disk is the tree the digest names. The files are written into a fresh hidden directory beside `destination`, which takes the name `destination` only once every file is written, so a failed or interrupted write leaves nothing under that name and removes what it wrote.
|
|
81
|
+
|
|
82
|
+
`destination` must not exist, or must be an empty directory: one holding anything is refused, rather than written into or replaced. A listing holding a path that is not a tree path, the same path twice, or two paths that collide once case-folded or where one names a directory of the other, is refused too. Each refusal is a `RequestArgumentError` thrown before any download. An empty listing writes an empty directory.
|
|
83
|
+
|
|
84
|
+
## When a transfer fails
|
|
85
|
+
|
|
86
|
+
`MethodFileTransferError` is thrown when a file could not be moved between this machine and storage. `operation` says which way (`upload` for a save, `download` for `writeMethodTree`), `code` why, `sha256` which bytes, `path` the tree path when one is known, and `status` storage's HTTP status when it answered. Neither its message nor any member carries the link, nor storage's error body, which can echo the signed request, and it keeps no `cause`: the failure `fetch` throws for a request that got no answer can quote the URL, so only its network code (`ECONNREFUSED`, `ABORT_TIMEOUT`) reaches the message.
|
|
87
|
+
|
|
88
|
+
| `code` | When | Retryable |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `unreachable` | Storage gave no answer: the connection failed or the time limit ran out. | yes |
|
|
91
|
+
| `storage_error` | Storage timed out or throttled the request (`408`, `429`) or failed (a `5xx` other than `501`). | yes |
|
|
92
|
+
| `refused` | Storage refused the request with any other status, an expired link among them. Read the method again for fresh links. | no |
|
|
93
|
+
| `mismatch` | A download's bytes are not the file the listing names: another size, or another SHA-256. | yes |
|
|
94
|
+
|
|
95
|
+
Its `errorDomain` is always `runtime`. An upload to a target that asks for a create-only write (`If-None-Match`) and answers `412` is not a failure: the bytes are already stored.
|
|
96
|
+
|
|
97
|
+
## What changed from the source fields
|
|
98
|
+
|
|
99
|
+
A method used to hold its source as two strings, `mthds` (a bundle, or the JSON text of a file array) and `python` (the JSON text of the custom PipeFunc files), and a version carried `source_digest`. Both fields are gone from every read and every write, with no alias kept: a method is a tree, a version's digest is `tree_digest`, and the publish state is derived from the two digests, the draft unchanged when `draft_digest` is not `null` and equals `latest_published.tree_digest`. `methodSourceToContents`, `getMethodClosure` and `EmptyMethodSourceError` went with them.
|
|
100
|
+
|
|
101
|
+
A run's `source_digest` is something else, and stays: the SHA-256 of the sources a run sent as text, 64 hex digits with no prefix. `RunDetail` also carries the run's `tree_digest` and `crate_fingerprint`, each `null` for a run that recorded none.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipelex/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "TypeScript SDK for the Pipelex hosted API — execute methods, manage runs, and call the product surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@eslint/js": "^9.39.4",
|
|
64
|
-
"@pipelex/tools-wasm": "0.
|
|
64
|
+
"@pipelex/tools-wasm": "0.6.0",
|
|
65
65
|
"@types/node": "^22.12.0",
|
|
66
66
|
"@vitest/coverage-v8": "^4.1.9",
|
|
67
67
|
"dependency-cruiser": "^17.4.3",
|
package/dist/method-source.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parse a stored method's polymorphic `MethodData.mthds` source into bundle
|
|
3
|
-
* file contents. The string is either raw `.mthds` source or a JSON-serialized
|
|
4
|
-
* `[{ name, content }, …]` file array (the webapp editor format).
|
|
5
|
-
*
|
|
6
|
-
* Mirrors the platform's canonical implementation — `method_source_to_contents`
|
|
7
|
-
* plus its caller's blank-source guard in `resolve_method_run_source`, both in
|
|
8
|
-
* the platform's `services/method_resolution.py` — keep the two in sync. A JSON
|
|
9
|
-
* `[]` is "no source", not a bundle; a JSON array of `{ name, content }`
|
|
10
|
-
* objects yields the non-blank contents; anything else (raw source, non-array
|
|
11
|
-
* JSON, unparseable) is one plain bundle string, itself dropped when blank.
|
|
12
|
-
* An empty result means the stored method has no MTHDS source.
|
|
13
|
-
*
|
|
14
|
-
* One deliberate divergence: the platform's blank-source guard is a falsy
|
|
15
|
-
* check (`if method.mthds`), so a whitespace-only raw source would pass there
|
|
16
|
-
* and fail downstream at parse; here it trims to "no source" — a clearer
|
|
17
|
-
* verdict for the same degenerate input.
|
|
18
|
-
*/
|
|
19
|
-
export declare function methodSourceToContents(mthds: string): string[];
|
package/dist/method-source.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parse a stored method's polymorphic `MethodData.mthds` source into bundle
|
|
3
|
-
* file contents. The string is either raw `.mthds` source or a JSON-serialized
|
|
4
|
-
* `[{ name, content }, …]` file array (the webapp editor format).
|
|
5
|
-
*
|
|
6
|
-
* Mirrors the platform's canonical implementation — `method_source_to_contents`
|
|
7
|
-
* plus its caller's blank-source guard in `resolve_method_run_source`, both in
|
|
8
|
-
* the platform's `services/method_resolution.py` — keep the two in sync. A JSON
|
|
9
|
-
* `[]` is "no source", not a bundle; a JSON array of `{ name, content }`
|
|
10
|
-
* objects yields the non-blank contents; anything else (raw source, non-array
|
|
11
|
-
* JSON, unparseable) is one plain bundle string, itself dropped when blank.
|
|
12
|
-
* An empty result means the stored method has no MTHDS source.
|
|
13
|
-
*
|
|
14
|
-
* One deliberate divergence: the platform's blank-source guard is a falsy
|
|
15
|
-
* check (`if method.mthds`), so a whitespace-only raw source would pass there
|
|
16
|
-
* and fail downstream at parse; here it trims to "no source" — a clearer
|
|
17
|
-
* verdict for the same degenerate input.
|
|
18
|
-
*/
|
|
19
|
-
export function methodSourceToContents(mthds) {
|
|
20
|
-
// The API types `mthds` as string, but mirror the platform's falsy guard so
|
|
21
|
-
// a contract-violating null/undefined reads as "no source", not a crash.
|
|
22
|
-
if (!mthds) {
|
|
23
|
-
return [];
|
|
24
|
-
}
|
|
25
|
-
let parsed;
|
|
26
|
-
try {
|
|
27
|
-
parsed = JSON.parse(mthds);
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return rawBundle(mthds);
|
|
31
|
-
}
|
|
32
|
-
if (Array.isArray(parsed)) {
|
|
33
|
-
if (parsed.length === 0) {
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
|
-
if (parsed.every(isFileEntry)) {
|
|
37
|
-
return parsed
|
|
38
|
-
.map((entry) => entry.content)
|
|
39
|
-
.filter((content) => typeof content === "string" && content.trim() !== "");
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return rawBundle(mthds);
|
|
43
|
-
}
|
|
44
|
-
function rawBundle(mthds) {
|
|
45
|
-
return mthds.trim() === "" ? [] : [mthds];
|
|
46
|
-
}
|
|
47
|
-
function isFileEntry(entry) {
|
|
48
|
-
return typeof entry === "object" && entry !== null && "name" in entry && "content" in entry;
|
|
49
|
-
}
|
|
50
|
-
//# sourceMappingURL=method-source.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"method-source.js","sourceRoot":"","sources":["../src/method-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAa;IAClD,4EAA4E;IAC5E,yEAAyE;IACzE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1B,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,IAAI,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;YAC9B,OAAO,MAAM;iBACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC;iBAC7B,MAAM,CACL,CAAC,OAAO,EAAqB,EAAE,CAAC,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,CACrF,CAAC;QACN,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,SAAS,CAAC,KAAa;IAC9B,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,IAAI,SAAS,IAAI,KAAK,CAAC;AAC9F,CAAC"}
|