@pipelex/sdk 0.34.1 → 0.35.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 +22 -2
- package/dist/cli/help.d.ts +3 -3
- package/dist/cli/help.js +6 -3
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/method.d.ts +9 -3
- package/dist/cli/method.js +30 -7
- package/dist/cli/method.js.map +1 -1
- package/dist/cli/script.d.ts +6 -2
- package/dist/cli/script.js +13 -6
- package/dist/cli/script.js.map +1 -1
- package/dist/client.d.ts +145 -28
- package/dist/client.js +215 -36
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +26 -5
- package/dist/errors.js +8 -5
- package/dist/errors.js.map +1 -1
- package/dist/execute-result.d.ts +6 -0
- package/dist/execute-result.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/method-selector.d.ts +42 -0
- package/dist/method-selector.js +63 -0
- package/dist/method-selector.js.map +1 -0
- package/dist/models.d.ts +8 -1
- package/dist/product-models.d.ts +211 -11
- package/dist/runs.d.ts +13 -0
- package/dist/runs.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +3 -2
- package/docs/cli.md +6 -6
- package/docs/crate-routes.md +4 -2
- package/docs/errors.md +17 -4
- package/docs/input-preparation.md +4 -2
- package/docs/run-results.md +1 -1
- package/package.json +1 -1
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/docs/architecture.md
CHANGED
|
@@ -82,6 +82,7 @@ A run request carries its method inline exactly one of three ways: `mthds_conten
|
|
|
82
82
|
- **Alone it is a run source; alongside an inline source it is linkage.** With no inline source the platform resolves and runs the stored method. With one, the inline source is what RUNS (precedence) and the id is recorded as run-history linkage on the Run row — the index key `GET /v1/runs?method_id=` queries. Rejecting the combination is not an option: it once orphaned every unsaved-bundle run from its method. The client therefore counts `method_id` towards its "something to run" precondition, and never treats it as an exclusivity violation.
|
|
83
83
|
- **It rides the blocking fallback too.** `startAndWaitForResult` degrades to `POST /v1/execute` against a runner with no run store, and the selector goes with it — so a bare `pipelex-api` answers a `422` that names the key (its Rule 4 diagnosis: "a hosted-only selector must be sent to the hosted API") instead of the client silently dropping it and running something else.
|
|
84
84
|
- **An empty string is treated as absent.** `method_id: ""` selects no method and links nothing, so it is not sent and does not satisfy the precondition.
|
|
85
|
+
- **It may name a version.** A bare `mt_…` runs the method's latest published version, `mt_…@<n>` the fixed version `n` and `mt_…@draft` its draft; the suffix is part of the string and passes through untouched, on the run routes and the tooling routes alike, since the platform parses it at its edge. On the linkage form, beside an inline source, the id must be bare: the inline source is what runs, so a suffix there would claim a version that did not run, and the platform refuses it with a `422`. The start ack and the execute response carry `method_version`, the version a `method_id` run runs, so a caller learns which version a bare id ran from the answer that started it. `parseMethodSelector` splits a selector into its bare id and version locally, for a caller that reads the method by its bare id.
|
|
85
86
|
|
|
86
87
|
## Module layout
|
|
87
88
|
|
|
@@ -124,13 +125,13 @@ Flat `src/` (mirrors `mthds-js`'s `runners/api` flatness — the SDK has one cli
|
|
|
124
125
|
The hosted catalog/account routes the webapp drives. All ride the same `{base}/v1/*` + `Authorization: Bearer` + org-from-JWT contract as the protocol routes, go through the shared `requestProduct<T>` helper (30s management timeout, empty-body tolerant), and map a non-2xx RFC 9457 `problem+json` to a typed `ApiResponseError`. Every route parses a 2xx body through one private helper, so an answer that is not JSON (a gateway's HTML page, an empty body where one was due) is an `ApiResponseError` carrying its status and raw text, `runtime` and not retryable, rather than a bare `SyntaxError`. A product route whose answer the SDK or its caller reads before anything else (`listMethods`, `listRuns`, `getRunDetail`, `upload`, `requestUploadGrant`, `resolveStorageUrls`, and the methods routes answering one stored method) goes through `requestProductObject` or a helper built on the same read instead, which also refuses an empty body and JSON that is not an object; the two paged routes then refuse, the same way, through one page reader, a page whose `items` is not an array or whose `next_cursor` is neither a string nor `null`, `upload` an answer with no non-empty string `uri`, and the methods routes a stored `mthds` field that is neither a string nor absent and a stored `python` field that is not the serialized file list. Consumers branch on **`type` and `errorDomain`**, as the hosted-envelope spec (`conformance/specs/pipelex-hosted-envelope.md`, in the `conformance` repository) says, never on the HTTP status. The platform's `type` is `https://pipelex.com/errors/<code>`, one-to-one with its native `code` (`conflict`, `not_found`, `pipelex_api_key_limit_reached`, …), which stays on the error as the finer surface-specific code. The platform does not emit `error_domain` or `retryable` on its own refusals yet, so their `errorDomain` and `retryable` are the SDK's fallback, read from the status; every error carries both, decided. See [errors.md](./errors.md).
|
|
125
126
|
|
|
126
127
|
- **User profile:** `getMe` (`GET /v1/me`).
|
|
127
|
-
- **Methods catalog:** `listMethods`, `getMethod`, `createMethod`, `
|
|
128
|
+
- **Methods catalog:** `listMethods`, `getMethod`, `createMethod`, `writeDraft`, `renameMethod`, `publishMethod`, `listMethodVersions`, `getMethodVersion`, `deleteMethod`. **A saved method has a draft and immutable published versions.** Its content fields (`mthds`, `python`, `input_data`) are its draft, which `writeDraft` (`PUT /v1/methods/{id}/draft`) replaces without validating it; `updated_at` is the draft's token, which moves on every draft write and nothing else, and `draft_digest` the SHA-256 of its two file sets. A draft write may carry `expected_updated_at`, the token the caller last saw, and is then a compare-and-swap: a draft that moved since is a `409` `method_update_conflict` and nothing is written. `renameMethod` (`PATCH /v1/methods/{id}`) changes the name alone and moves no token. `publishMethod` (`POST /v1/methods/{id}/publish`) takes the required `expected_draft_updated_at` and answers a `MethodPublishResult` discriminated on `outcome` — `published` with the new version, `unchanged` with the existing latest version when the draft's digest equals it, `refused` with a `reason` (`invalid` or `not_runnable`), a `message` and the runner's `validation` verdict — every arm carrying the `method`; the SDK reads the nested method as `getMethod` does, and an answer whose `outcome` is none of these is an answer it cannot read. A publish that produced no verdict is thrown rather than answered: only a draft that differs from the latest version reaches the runner, and a runner that cannot be reached, answers unusably or does not finish within the platform's deadline is a `502` or `503` with nothing written, so a retry is safe, and a retry of a publish that landed while its answer was lost answers `unchanged` with the version it wrote. A rename can be a `413` `payload_too_large` for a name so long it would leave the method too large to publish. A version is numbered from 1, never reused, and holds the draft's files as they stood, its `source_digest`, its `crate_fingerprint` (`null` when the runner could not resolve the bundle in memory), the runner's version, and when and by whom it was published; `listMethodVersions` pages the summaries newest first (`MethodVersionPage`, `{items, nextCursor}`), and `getMethodVersion` reads one with its sources, its `python` parsed as a method's is. A method read carries `latest_version` and `latest_published`, both `null` until the first publish, so the publish state is derived: never published, draft unchanged (`draft_digest === latest_published.source_digest`), or draft ahead. The method routes take a bare id and address the method itself, never one of its versions; the platform does not parse a suffix there, so `mt_x@3` would answer a `404` that reads as a method that does not exist, and each route refuses a suffixed id with `RequestArgumentError` before anything is sent, naming `parseMethodSelector` and `getMethodVersion`. The whole-method `updateMethod` is gone, replaced by the draft write and the rename. **`deleteMethod` is asynchronous and its return type says so:** `DELETE /v1/methods/{id}` answers `202` with a `MethodDeletionAccepted` (`method_id`, `deletion_state`, `deletion_job_id`) the moment the platform has claimed the method and terminated its in-flight workflows — the rest of the cascade (runs, events, S3 objects) is enqueued. A resolved promise means "accepted", never "gone"; completion is the row disappearing from `listMethods`, not any field of that body. The claim is a conditional write, so a double-clicked delete is a `409 method_being_deleted` rather than a second cascade over the same runs, and an unknown or foreign-org id is a `404`. The erasure deletes the method's versions with the rest. (This doc previously stated the SDK had no `delete` because the platform had no such route — the route has always existed; the claim was simply false.) `listMethods` returns **one page**, not the whole catalog — a `MethodPage` (`{items, nextCursor}`) served from a narrow index projection, so its cost is the page size rather than the size of the catalog. It used to return every method with its full `.mthds` bundle attached, which hit DynamoDB's 1 MB page cap at a couple hundred methods and came back truncated with no error and no flag, so methods silently vanished from the UI. Rows are therefore `MethodSummary`, **not** `MethodData`: no `mthds`, no `python`, no `updated_at`, because none of them is in the projection and putting `mthds` back is what restored the truncation. Reach for `getMethod(id)` when you need a method's source. `query.q` is applied server-side across the whole catalog, not over the page — filtering one page client-side would be searching 50 rows of 10,000 and calling it a search. `iterateMethods` is the async iterator that follows the cursor for callers who genuinely want everything (`for await (const m of client.iterateMethods())`); unlike `iterateRuns` it must keep going **past empty pages**, because `q` is a post-read filter over a bounded index slice per request, so `{items: [], nextCursor: "…"}` means "nothing matched in the slice I just read, keep going" — the difference is the server, not the client. A server that keeps minting fresh cursors forever stops it at a page ceiling far past any real catalog with a `PagingNotTerminatingError` carrying `pageLimit`, never a silently truncated stream. It gives up only when the server stops advancing its cursor, and **throws** rather than returning a partial answer; like `iterateRuns` it is deliberately not a `listAllMethods(): Promise<MethodSummary[]>`, because an all-at-once helper needs a page cap and a cap means silently returning a truncated list. `MethodSummary.deletion_state` rides the list rows: a method mid-erasure stays listed so the UI can render it as "Deleting…", while `getMethod` refuses it with a 409. Full read responses (`MethodData`, from `getMethod`) carry `org_id`, `created_by_user_id`, a server-derived `description` (recomputed from the draft's top-level `description` on every draft write), the draft token and digest and the latest version's summary on top of the write contract's `name` / `mthds` / `input_data`. `getMethodClosure` is a client-side semantic layer over `getMethod` (not a route) that parses the draft's polymorphic `mthds` source into a runnable, provenance-labelled closure — the draft, not what a bare id runs — see [input-preparation.md](./input-preparation.md#getmethodclosure--the-explicit-expansion-utility).
|
|
128
129
|
- **Organizations:** `listMemberships`, `createOrganization`, `renameOrganization` (`PATCH`). The org "switch" is a WorkOS-session op (`/api/active-org`), not a `/v1` route — out of SDK scope.
|
|
129
130
|
- **Billing:** `getSubscription`, `listPlans`, `listInvoices`, `createCheckout`, `changePlan` (409 `conflict` ⇒ no subscription), `getBillingPortal` (409 `conflict` ⇒ no subscription).
|
|
130
131
|
- **Pipelex API keys (`plx_sk_…`):** `listPipelexApiKeys`, `createPipelexApiKey` (409 `pipelex_api_key_limit_reached`), `revokePipelexApiKey`, `rotatePipelexApiKey`. The plaintext `api_key` is returned once, on create/rotate.
|
|
131
132
|
- **Onboarding:** `submitOnboarding`.
|
|
132
133
|
- **Storage:** `resolveStorageUrl`, `resolveStorageUrls` (`POST /v1/resolve-storage-url/bulk` — the single route applied to a list of at most 100 references, one verdict per reference in request order, a refused reference a value on its item; a deployment without the route answers a `404` `ApiResponseError`), `upload` (JSON base64 — the multipart hop is browser→BFF only), `requestUploadGrant` (`POST /v1/upload/grant` — a presigned, create-only `PUT` for one new object, which the holder of the bytes sends with the standalone `uploadWithGrant`, also exported from the browser-safe `@pipelex/sdk/upload` entry; a declared size over the cap is a `413` `ApiResponseError`). The higher-level `uploadFile` / `prepareInputs` preparation surface is built on top of `upload`; `prepareInputs` additionally composes one `pipeIo` call, which selects the pipe and answers with the input-form descriptor it walks, and takes the method as inline `files`, a `method_ref` or a `method_id` like every other method-taking operation — see [input-preparation.md](./input-preparation.md). The download twin — `locateArtifacts`, `collectArtifacts`, `resolveArtifacts`, `fetchArtifact`, `downloadArtifacts` — is built on top of `resolveStorageUrls` and `getRunResult`, never on the embedded `public_url`, and is documented on [artifact-download.md](./artifact-download.md); downloading is a separate explicit gesture, and no run call takes a `download` option.
|
|
133
|
-
- **Runs list/detail/update:** `listRuns` (by `method_id`) returns **one page**, not the whole history — a `RunPage` (`{items, nextCursor}`) served from a time-ordered index, so its cost is the page size rather than the size of the history. Its rows are `RunHistoryItem`, **not** `PipelineRun`: the id, `status`, `created_at`, `finished_at`, `pipe_code
|
|
134
|
+
- **Runs list/detail/update:** `listRuns` (by `method_id`) returns **one page**, not the whole history — a `RunPage` (`{items, nextCursor}`) served from a time-ordered index, so its cost is the page size rather than the size of the history. Its rows are `RunHistoryItem`, **not** `PipelineRun`: the id, `status`, `created_at`, `finished_at`, `pipe_code`, for a failed run `error`, and `method_version` and `source_digest`, which version of the method the run ran (a number, `"draft"`, or `null` for an inline source) and the digest of what it ran — what a history row shows. The history lists the runs of every version and of the draft together, by the bare id. `method_version` and `source_digest` are optional on every run record (`RunPublic`, `RunRead`, `PipelineRun`, `RunDetail`, `RunHistoryItem`), since a run recorded before the platform recorded them carries neither. The run's organization, creator, method, workflow id and result location are not on the list; `getRunDetail` returns them. `iterateRuns` is the async iterator that follows the cursor for callers who genuinely want everything (`for await (const run of client.iterateRuns(id))`); it is deliberately not a `listAllRuns(): Promise<RunHistoryItem[]>`, because an all-at-once helper needs a page cap and a cap means silently returning a truncated list. Both accept `createdFrom` / `createdTo`, applied server-side as index key conditions — these are **instants, not days** (ISO-8601 with a UTC offset; a naive timestamp is a 400), because only the caller knows which timezone's day it means. `getRunDetail` (`GET /v1/runs/{id}`) is the only call returning `mthds_contents` — what the run actually executed — and `inputs`; it is kept off the status read that pollers hit every few seconds and off the list, where the bundle would be multiplied by the page size. `updateRun` is the admin/manual status patch. The status/results/start lifecycle routes live in the run-lifecycle section above.
|
|
134
135
|
|
|
135
136
|
## Hook bundle (`dist-hooks/check.mjs`)
|
|
136
137
|
|
package/docs/cli.md
CHANGED
|
@@ -19,7 +19,7 @@ The command lives in `src/cli.ts`, the executable, and `src/cli/`. It sits at th
|
|
|
19
19
|
pipelex-sdk run --method <method> [--pipe <domain.pipe_code>] [--inputs <file> | --inputs-template]
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
- **`--method`**, required, names the method in one of the forms below, told apart by shape alone. A value starting with `mt_` is a catalog id, sent as `method_id`. A value starting with `github.com/` is a published method's address, `github.com/<owner>/<repo>[/<package>][@<tag>]`, sent as `method_ref`. Anything else is a path to a `.mthds` file or a bundle directory, which must exist and is read from disk and sent inline (see "Reading a bundle" below). A file or directory that happens to be named `mt_…` is reached as `./mt_…`, so what a value names never depends on what the current directory holds, and an address is written without its scheme. An `mt_` value holding a character no catalog id holds, anything but letters, digits, `_` and `-` (the ids are minted as `mt_` and a UUID, and the run route takes no other character), is a usage error made before any request, which says to write `./` to name a file: `mt_review.mthds` is refused, `./mt_review.mthds` is a path.
|
|
22
|
+
- **`--method`**, required, names the method in one of the forms below, told apart by shape alone. A value starting with `mt_` is a catalog id, sent as `method_id`. A value starting with `github.com/` is a published method's address, `github.com/<owner>/<repo>[/<package>][@<tag>]`, sent as `method_ref`. Anything else is a path to a `.mthds` file or a bundle directory, which must exist and is read from disk and sent inline (see "Reading a bundle" below). A file or directory that happens to be named `mt_…` is reached as `./mt_…`, so what a value names never depends on what the current directory holds, and an address is written without its scheme. An `mt_` value holding a character no catalog id holds, anything but letters, digits, `_` and `-` (the ids are minted as `mt_` and a UUID, and the run route takes no other character), is a usage error made before any request, which says to write `./` to name a file: `mt_review.mthds` is refused, `./mt_review.mthds` is a path. A catalog id may end in a version suffix, which is sent whole: a bare `mt_…` runs the method's latest published version, `mt_…@<n>` the fixed version `n` and `mt_…@draft` its draft. A suffix that is neither a positive number without a leading zero nor `draft` (`@0`, `@03`, `@Draft`, two suffixes) is a usage error made before any request, as `parseMethodSelector` reads it.
|
|
23
23
|
- **`--pipe`** names the pipe by its qualified ref, `domain.pipe_code`. It is sent as `pipe_ref` to the input preparation and as `pipe_code` to the run. A bare code, and an `alias->domain.pipe_code` ref naming a dependency's pipe, are usage errors. Without it, the run takes the method's main pipe, as the API resolves it.
|
|
24
24
|
- **`--inputs`** names a JSON file holding one object, one entry per input, the shape `mthds run --inputs` and `cli-python`'s `--inputs` take; `-` reads it from stdin. The text is UTF-8, and a byte-order mark at its start is dropped, as RFC 8259 lets a JSON reader do, since Windows PowerShell 5.1 writes one (`Out-File -Encoding utf8`); a bundle's mark, by contrast, is sent as it is on disk. A relative path resolves against the current directory. A file or stdin that cannot be read is a usage error, `cannot read stdin.` with the system's reason under it (`Reason: EIO`), and a stdin closed when the command starts reads as empty, so it is not valid JSON: Node opens the null device on a standard stream a process starts without, and the Python command reads the missing stream the same way. A `process.stdin` destroyed before the command reads it is unreadable too, with the reason `ERR_STREAM_PREMATURE_CLOSE`. Without it the run gets no inputs. When the object has entries, they first go through the SDK's input preparation (`prepareInputs`, one `POST /v1/pipe-io` call): a local path or a `data:` URL at a file input is uploaded and replaced by its `pipelex-storage://` reference, each upload is reported on stderr, and a URL passes through. An empty object or no `--inputs` makes no preparation request. A value at a file input that is no file, a `data:` URL that does not decode or a value of another type such as a number, is a usage error, as is a local path that cannot be read; a method that does not load, met during the preparation, is reported as `--inputs-template` reports it ("Exit codes, errors and Ctrl-C" below). The command tells the two apart by the SDK's error classes, `InvalidLocalSourceError` and `InvalidInputValueError` for the inputs' mistakes and `MethodLoadError` for the method's ([`input-preparation.md`](input-preparation.md)).
|
|
25
25
|
- **`--inputs-template`** prints the pipe's inputs template, the object `--inputs` takes with a placeholder for every input, and runs nothing. The template is rendered by `mthds`' `renderInputsTemplate`, in its compact JSON shape, from the input-form descriptor one `POST /v1/pipe-io` call returns, which spends no inference. It takes no `--inputs`.
|
|
@@ -41,13 +41,13 @@ pipelex-sdk script --method <address | catalog id> [--pipe <domain.pipe_code>] [
|
|
|
41
41
|
# receipt-review: runs github.com/acme/methods/receipt-review@v1.0.0 on the Pipelex API.
|
|
42
42
|
# Written by @pipelex/sdk X.Y.Z. Needs Node 22.12 or later, and PIPELEX_API_KEY in the environment.
|
|
43
43
|
# Usage: ./receipt-review --inputs inputs.json, or ./receipt-review --inputs-template to see what to fill in.
|
|
44
|
-
exec npx --yes @pipelex/sdk@X.Y.Z run --method 'github.com/acme/methods/receipt-review@v1.0.0' --pipe 'receipts.review_receipt' "$@"
|
|
44
|
+
exec npx --yes --no-update-notifier --loglevel=warn @pipelex/sdk@X.Y.Z run --method 'github.com/acme/methods/receipt-review@v1.0.0' --pipe 'receipts.review_receipt' "$@"
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
- **The method is an address or a catalog id.** A local bundle is refused, since a script naming a path breaks as soon as the script or the bundle moves; the refusal names both ways to get one, publishing the method or saving it to the catalog. An address without a tag is accepted, with a warning that the script follows the repository's default branch.
|
|
48
|
-
- **The name** defaults to the address's last segment without its tag, or, for a catalog id, to the method's name read from its catalog entry (`GET /v1/methods/{id}
|
|
48
|
+
- **The name** defaults to the address's last segment without its tag, or, for a catalog id, to the method's name read from its catalog entry (`GET /v1/methods/{id}`, by the bare id, since the name belongs to the method and not to a version; the script keeps the suffix) and kebab-cased: decomposed (NFKD), stripped of combining marks, lowercased, every run of characters outside `a-z0-9` turned into one `-`, trimmed of `-`. A name that comes out empty asks for `--name`, and so does a catalog entry whose name is missing or not a string. A name is a file name: it may not be empty, `.` or `..`, or hold a `/`, a `\` or a control character. The `\` is refused on every platform, since Windows reads it as a `/` and `..\outside` would leave `--dir`.
|
|
49
49
|
- **The file** is written in `--dir`, by default the current directory, which must exist. It is created with the permissions of an executable (`0755`, less the umask) and never over an existing file, a dangling link included. A write that fails once the file exists removes it, so a script is whole or absent, never a truncated one that would run half a command and make the next attempt say it already exists; a Ctrl-C waits for the file to be whole ("Exit codes, errors and Ctrl-C" below). The path printed is `--dir` as given without its trailing `/`, or `.`, then `/` and the name.
|
|
50
|
-
- **The script's one line** is `exec npx --yes @pipelex/sdk@<version> run --method '<method>' [--pipe '<pipe>'] "$@"`: every value single-quoted, an inner `'` spelled `'\''`, and no value may hold a control character. The `"$@"` passes everything else on, `--inputs`, `--inputs-template` and stdin included, and since `run` refuses a flag given twice, a second `--method` cannot quietly run another method. The version is the one of the SDK that wrote the script, so the script keeps running the command it was written with; `npx` caches the package, so after the first run only the API needs the network. There is no option to write the Python SDK's line, since this SDK cannot know which version of the other package exists and was tested.
|
|
50
|
+
- **The script's one line** is `exec npx --yes --no-update-notifier --loglevel=warn @pipelex/sdk@<version> run --method '<method>' [--pipe '<pipe>'] "$@"`: every value single-quoted, an inner `'` spelled `'\''`, and no value may hold a control character. The `"$@"` passes everything else on, `--inputs`, `--inputs-template` and stdin included, and since `run` refuses a flag given twice, a second `--method` cannot quietly run another method. The version is the one of the SDK that wrote the script, so the script keeps running the command it was written with; `npx` caches the package, so after the first run only the API needs the network. The two flags keep npm's own notices off the script's stderr, which carries the command's lines: `--no-update-notifier` removes the update notice npm 10 and 11 print wherever their cache starts empty, as in a fresh container, and `--loglevel=warn` removes the `npm notice run …` lines npm 12 prints at every run, while npm's warnings and errors, such as a version that cannot be fetched or a Node older than the package needs, still print. They act on `npx` alone, which hands everything after the package to the command. There is no option to write the Python SDK's line, since this SDK cannot know which version of the other package exists and was tested.
|
|
51
51
|
- **On Windows** the script needs a POSIX shell, such as Git Bash or WSL; otherwise type the `npx` line directly.
|
|
52
52
|
|
|
53
53
|
## The environment
|
|
@@ -66,7 +66,7 @@ The exit code is presentation, following the workspace's surface-output rule: a
|
|
|
66
66
|
| --- | --- |
|
|
67
67
|
| `0` | The result was printed, or the script written, or the help or version shown |
|
|
68
68
|
| `1` | The run failed, the API refused a request, could not be reached, or answered what the command cannot read |
|
|
69
|
-
| `2` | A usage error: a bad flag, an unreadable or invalid inputs file, a selector of no known form, a path that names no bundle, a missing key, a refused base URL, an `mt_` value that cannot be a catalog id, a local file the inputs name that cannot be read, a value at a file input that is no file, a script that would overwrite a file |
|
|
69
|
+
| `2` | A usage error: a bad flag, an unreadable or invalid inputs file, a selector of no known form, a path that names no bundle, a missing key, a refused base URL, an `mt_` value that cannot be a catalog id or names no version, a local file the inputs name that cannot be read, a value at a file input that is no file, a script that would overwrite a file |
|
|
70
70
|
| `130` | Interrupted: on Linux and macOS the process ends by the interrupt signal, which a shell reports as `130` |
|
|
71
71
|
|
|
72
72
|
Every error goes to stderr, its first line `Error: <sentence>` and the lines under it giving the details. The command words a failure from the error's typed fields rather than from an SDK message, wherever the fields carry what a person needs, so that the JavaScript and Python commands print the same lines for the same answer:
|
|
@@ -108,7 +108,7 @@ The table holds:
|
|
|
108
108
|
|
|
109
109
|
- **`base_url`**, the origin every recorded route is served on, and **`env`**, the environment every case starts from: a key, that base URL and the poll interval `0`.
|
|
110
110
|
- **`answers`**, named answers a route's exchange can refer to rather than repeat.
|
|
111
|
-
- **`placeholders`**, each with its JavaScript and Python value: `WRITER` (`@pipelex/sdk`), `NEEDS` (`Node 22.12 or later`), `RUNNER` (`npx --yes @pipelex/sdk@{{SDK_VERSION}}`), and `SDK_VERSION`, which each suite fills with its own SDK's version. They appear as `{{NAME}}` in a case's expected outputs only, and each suite substitutes the language values first, then `{{SDK_VERSION}}`.
|
|
111
|
+
- **`placeholders`**, each with its JavaScript and Python value: `WRITER` (`@pipelex/sdk`), `NEEDS` (`Node 22.12 or later`), `RUNNER` (`npx --yes --no-update-notifier --loglevel=warn @pipelex/sdk@{{SDK_VERSION}}`), and `SDK_VERSION`, which each suite fills with its own SDK's version. They appear as `{{NAME}}` in a case's expected outputs only, and each suite substitutes the language values first, then `{{SDK_VERSION}}`.
|
|
112
112
|
- **`cases`**, each with these fields:
|
|
113
113
|
- `name`, unique, and `summary`, one sentence saying what the case holds.
|
|
114
114
|
- `argv`, the arguments after the command's name.
|
package/docs/crate-routes.md
CHANGED
|
@@ -33,7 +33,7 @@ Treat that attribution as best-effort, not a guarantee — which is why `Validat
|
|
|
33
33
|
|
|
34
34
|
An **address-form** `method_ref` (`github.com/<owner>/<repo>[/<selector>][@<tag>]`) is resolved by the server through the same fetch path as a `method_ref` run: the repository fetched at the tag, the package located by manifest identity, the package's real relative paths feeding the per-file `source` labels. Any non-address reference stays reserved and answers `501`.
|
|
35
35
|
|
|
36
|
-
`method_id` is the hosted platform's selector, and it is a **pass-through**: nothing is expanded client-side — the platform resolves the id against the org's catalog and injects the stored source before the runner sees the request. It is meaningless against a bare runner (no catalog), and on `api.pipelex.com` its availability follows the platform deploy that adds the tooling-route transform. An unknown or foreign-org id is a `404` (indistinguishable by design); a stored method with no MTHDS source is a `422`.
|
|
36
|
+
`method_id` is the hosted platform's selector, and it is a **pass-through**: nothing is expanded client-side — the platform resolves the id against the org's catalog and injects the stored source before the runner sees the request. A bare `mt_…` names the method's latest published version, `mt_…@<n>` the fixed version `n` and `mt_…@draft` its draft, and the suffix rides the string untouched. It is meaningless against a bare runner (no catalog), and on `api.pipelex.com` its availability follows the platform deploy that adds the tooling-route transform. An unknown or foreign-org id is a `404` (indistinguishable by design); a bare id of a method never published is a `409` `method_not_published`, whose detail names `mt_…@draft`; a version the method never published is a `404` `method_version_not_found`; a malformed suffix is a `422`; a stored method with no MTHDS source is a `422`.
|
|
37
37
|
|
|
38
38
|
Supplying **no** selector or **more than one** is a request-shape `422` — the tooling routes are stateless, so there is no linkage exception; a second selector could only be ignored, which is the worst contract of the three. The SDK does not model the XOR in the type system — the union would force the overwhelmingly common `{ files }` call site to pick a branch for no gain, and the server's answer is a typed `ApiResponseError` either way.
|
|
39
39
|
|
|
@@ -134,9 +134,11 @@ Only a **no-verdict** condition, as the typed `ApiResponseError` — branch on i
|
|
|
134
134
|
|
|
135
135
|
| Status | Cause |
|
|
136
136
|
| ------------- | --------------------------------------------------------------------------------------------------------- |
|
|
137
|
-
| `422` | Request shape: no closure selector or more than one, an over-limit file, an unknown `kind`/`target`, a `pipe_ref` on `kind: "types"`, a fetched package with no `.mthds` file, a stored method with no MTHDS source. |
|
|
137
|
+
| `422` | Request shape: no closure selector or more than one, an over-limit file, an unknown `kind`/`target`, a `pipe_ref` on `kind: "types"`, a fetched package with no `.mthds` file, a stored method with no MTHDS source, a `method_id` whose version suffix is malformed. |
|
|
138
138
|
| `422` | On `pipeIo`, a pipe selection the route refuses: `errorType` `EntryPipeNotFoundError` for a `pipe_ref` that names no pipe or a method with no entry pipe, `EntryPipeAmbiguousError` for a code that matches pipes in several domains or several `main_pipe` declarations. The `serverMessage` names the candidates where there are some. |
|
|
139
139
|
| `404` | Unknown or foreign-org `method_id` (indistinguishable by design); no package at a `method_ref` address. |
|
|
140
|
+
| `404` | `code` `method_version_not_found`: a `mt_…@<n>` naming a version the method never published. |
|
|
141
|
+
| `409` | `code` `method_not_published`: a bare `method_id` of a method never published; `mt_…@draft` reaches its draft. |
|
|
140
142
|
| `501` | Registry-form `method_ref` — reserved, not implemented (the address form resolves). |
|
|
141
143
|
| `401` / `403` | Auth, or on a hosted origin a route the gateway does not list yet (`403 {"message":"Forbidden"}`). |
|
|
142
144
|
| `502` | On a hosted origin, a cold `method_ref` clone that outran the gateway's 30-second cap; a retry clears it. |
|
package/docs/errors.md
CHANGED
|
@@ -41,7 +41,7 @@ try {
|
|
|
41
41
|
| Class | `errorDomain` | `retryable` | Why |
|
|
42
42
|
|---|---|---|---|
|
|
43
43
|
| `ApiResponseError` | the server's, else the fallback | the server's, else no for a sent `input` or `config` domain, else the fallback | See [below](#a-refused-requests-verdict). |
|
|
44
|
-
| `RequestArgumentError` | `input` | no | The client refused a call's arguments before sending any request: no run source given to `execute` or `start`, run sources or method selectors that exclude each other, a selector rule of `validate`, an empty `validateFiles`, a reserved key in `extra
|
|
44
|
+
| `RequestArgumentError` | `input` | no | The client refused a call's arguments before sending any request: no run source given to `execute` or `start`, run sources or method selectors that exclude each other, a selector rule of `validate`, an empty `validateFiles`, a reserved key in `extra`, a `publishMethod` without its token, a `getMethodVersion` version that is not a positive integer, or a selector `parseMethodSelector` refuses. Its options take an optional `verdict`, and the client declares `config` for a base URL that is not host-only, since it typically comes from `PIPELEX_BASE_URL`. That refusal shows the base URL's scheme and host alone and names the other parts it carried (credentials, a path, a query, a fragment) without their text, since those are where a secret travels in a URL; a value that is not an http or https URL is not shown at all. It is a `PipelineRequestError`, as the bare refusal it replaced was, and a refusal the standard's own run-source check raises keeps that original as `cause`. |
|
|
45
45
|
| `ApiUnreachableError` | `config` | yes | The address or the network must be checked, and a later attempt can get through. With the code `ABORT_TIMEOUT`, the SDK's own request timeout, it is `runtime`: the API took the request and did not answer in time. |
|
|
46
46
|
| `PipelineExecuteTimeoutError` | `input` | no | The blocking `execute` cannot outlast the gateway's limit; start the run and poll instead. |
|
|
47
47
|
| `RunFailedError` | the report's | the report's | See [a failed run](#a-failed-run--runerrorreport). |
|
|
@@ -71,7 +71,7 @@ The fallback reads the HTTP status, the platform's `code`, and whether the body
|
|
|
71
71
|
| `404`, bare | `config` | no | The base URL points at a deployment without the route. |
|
|
72
72
|
| `405` | `config` | no | The deployment does not serve this method on the route. |
|
|
73
73
|
| `408`, `429` | `runtime` | yes | Refused for its timing, not its content. |
|
|
74
|
-
| `409` | `input` | no | The request conflicts with the stored state: a method being deleted, a stale
|
|
74
|
+
| `409` | `input` | no | The request conflicts with the stored state: a method being deleted, a stale draft token, a method never published addressed by its bare id, a taken label. |
|
|
75
75
|
| `409` with code `pipelex_api_key_limit_reached` | `config` | no | The organization holds as many keys as it may; someone removes one. |
|
|
76
76
|
| `501` | `config` | no | The deployment does not implement it, and asking again will not change that. |
|
|
77
77
|
| `500`, `502`, `503`, `504`, and any `5xx` not listed here | `runtime` | yes | A fault in the service, which may pass. |
|
|
@@ -155,7 +155,7 @@ A field a newer runner adds is reachable through the interface's index signature
|
|
|
155
155
|
|
|
156
156
|
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).
|
|
157
157
|
|
|
158
|
-
**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. 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`, `
|
|
158
|
+
**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. 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`, `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` or `listRuns` whose `items` is not an array or whose `next_cursor` is neither a string nor `null`, which `iterateMethods` and `iterateRuns` 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, which a caller branching on `outcome` could not read; an `upload` answer with no non-empty string `uri`, which names no stored file; and a stored method or a published version whose `mthds` field is neither a string nor absent (a `null` one is still no source, which `getMethodClosure` reports as `EmptyMethodSourceError`) or whose `python` field is not the serialized `[{ name, content }]` list, as `getMethod`, `createMethod`, `writeDraft`, `renameMethod`, `publishMethod` and `getMethodVersion` answer it: 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, or `mthds`'s refusal of the `python` field; nothing for a body that parsed to something the route does not answer), `problemDocument` and the problem members are `undefined`, and its verdict is the fallback's for a status no refusal carries: `runtime`, not retryable. 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.
|
|
159
159
|
|
|
160
160
|
| Field | Wire member | Meaning |
|
|
161
161
|
|---|---|---|
|
|
@@ -167,7 +167,7 @@ Every non-2xx answer from a `/v1` route is a problem document, and the SDK throw
|
|
|
167
167
|
| `title` | `title` | The stable human label of the error class. |
|
|
168
168
|
| `serverMessage` | `detail` (or `message`) | The per-occurrence message. |
|
|
169
169
|
| `instance` | `instance` | The occurrence: a request URN (`urn:pipelex:request:<id>`) or a request path. |
|
|
170
|
-
| `code` | `code` | The platform's native code, a closed set (`conflict`, `not_found`, `validation_failed`, `pipelex_api_key_limit_reached`, …), one-to-one with `type
|
|
170
|
+
| `code` | `code` | The platform's native code, a closed set (`conflict`, `not_found`, `validation_failed`, `pipelex_api_key_limit_reached`, …), one-to-one with `type`; `MethodErrorCode` names the method ones (see [The method codes](#the-method-codes)). |
|
|
171
171
|
| `errorType` | `error_type` | The runner's native code, its open exception class name. |
|
|
172
172
|
| `errorCategory`, `model`, `provider`, `providerMetadata` | `error_category`, `model`, `provider`, `provider_metadata` | An inference failure's class and origin, as on the run report. |
|
|
173
173
|
| `migration` | `migration` | A pending configuration migration that explains the failure. |
|
|
@@ -197,6 +197,19 @@ try {
|
|
|
197
197
|
|
|
198
198
|
The members `mthds`'s own `ApiResponseError` carries (`type`, `title`, `instance`, `requestId`, `errorDomain`, `retryable`, `userAction`) have the same names here, and the same types except the verdict: the standard's client leaves `errorDomain` an open string and `retryable` optional, where this SDK decides both. A type-level test pins `ProblemDetails`, the members as parsed, to the standard client's declaration, so a consumer reads one vocabulary whichever client raised the error. The rest are the Pipelex members the standard's client leaves to this SDK.
|
|
199
199
|
|
|
200
|
+
### The method codes
|
|
201
|
+
|
|
202
|
+
A refusal about a saved method or one of its versions carries one of the platform codes the exported type `MethodErrorCode` names. `code` itself stays a `string`, since the platform adds codes; every one of these is `input` and not retryable by the fallback, since asking again unchanged meets the same answer.
|
|
203
|
+
|
|
204
|
+
| `code` | Status | When | What the caller does |
|
|
205
|
+
|---|---|---|---|
|
|
206
|
+
| `method_update_conflict` | `409` | The draft moved since the token a `writeDraft` (`expected_updated_at`) or a `publishMethod` (`expected_draft_updated_at`) sent; nothing was written or published. `errors[0].field` names the request field that carried the token. | Read the method again, and either load its draft or overwrite it by writing again with the fresh token. |
|
|
207
|
+
| `method_being_deleted` | `409` | The method's erasure has started, on every route that addresses it; the detail tells an erasure in flight from a failed one. | Stop offering the method; retry the delete when the detail says it failed. |
|
|
208
|
+
| `method_not_published` | `409` | A bare `method_id`, which names the latest published version, on a run or tooling route, for a method never published. The detail names `mt_…@draft` and publishing. | Publish the method, or address its draft as `mt_…@draft`. |
|
|
209
|
+
| `method_version_not_found` | `404` | `mt_…@<n>`, or `getMethodVersion`, names a version the method never published. An unknown method is `not_found` instead, so the two are told apart by the code. | Pick a version `listMethodVersions` lists. |
|
|
210
|
+
|
|
211
|
+
A malformed version suffix (`@0`, `@03`, `@Draft`, two suffixes) is a `422` `validation_failed` whose `errors` name the field `method_id`; `parseMethodSelector` refuses the same selectors locally with a `RequestArgumentError`, before anything is sent.
|
|
212
|
+
|
|
200
213
|
## A validation item — `ValidationErrorItem`
|
|
201
214
|
|
|
202
215
|
A method refused for what it says, rather than for how its run went, comes back as a list of validation items. They ride the `/v1/validate` verdict (its invalid arm's `validation_errors[]`, and the advisory `warnings[]` of its valid arm) and the crate routes' invalid verdict (`CrateInvalidReport.validation_errors[]`), each a `200` because an invalid bundle is a produced verdict; the `422` a run route answers when the runner refuses the method for its validation errors, as `ApiResponseError.validationErrors`; and a failed run's report, as `validation_errors`. Each item has a `category` and a `message`; the other members locate the error and say what to do next, and each is present only when the runner knows it:
|
|
@@ -149,7 +149,9 @@ There is no single-pipe fallback: a method that declares one pipe and no `main_p
|
|
|
149
149
|
client.getMethodClosure(methodId) → MthdsFileItem[]
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
`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 polymorphic `mthds` source with [`methodSourceToContents`](#methodsourcetocontents--the-canonical-source-parser), and labels each resulting file with the `method_id` as its `source` provenance.
|
|
152
|
+
`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.
|
|
153
|
+
|
|
154
|
+
**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.
|
|
153
155
|
|
|
154
156
|
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:
|
|
155
157
|
|
|
@@ -157,7 +159,7 @@ Reach for it whenever you want the files **in hand** — to edit, to diff, or to
|
|
|
157
159
|
runner.resolve({ files: await hosted.getMethodClosure("mt_…") })
|
|
158
160
|
```
|
|
159
161
|
|
|
160
|
-
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: the platform resolves it and injects the
|
|
162
|
+
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*.
|
|
161
163
|
|
|
162
164
|
- **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.
|
|
163
165
|
- **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.
|
package/docs/run-results.md
CHANGED
|
@@ -70,7 +70,7 @@ const results = await client.startAndWaitForResult(
|
|
|
70
70
|
);
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
It is never called on the blocking path, a bare runner's `POST /v1/execute` or the fallback to it, which has no run id to give before it answers. The acknowledgement is handed over whole, a `method_ref` run's `method_provenance` included. The callback runs synchronously and its return value is ignored; an exception it throws propagates out of `startAndWaitForResult` before anything is polled, and the run it was told about keeps going.
|
|
73
|
+
It is never called on the blocking path, a bare runner's `POST /v1/execute` or the fallback to it, which has no run id to give before it answers. The acknowledgement is handed over whole, a `method_ref` run's `method_provenance` and a `method_id` run's `method_version` (the version the run runs, a number or `"draft"`) included. The callback runs synchronously and its return value is ignored; an exception it throws propagates out of `startAndWaitForResult` before anything is polled, and the run it was told about keeps going.
|
|
74
74
|
|
|
75
75
|
**An abort before the run exists creates none.** The `signal` is read before the run is started as well as during the wait: a caller that aborts while the `GET /v1/version` handshake is in flight gets its abort, and neither `POST /v1/start` nor the blocking `POST /v1/execute` is sent; nor is the execute when the abort lands while a runner that looked hosted refuses the start. Once the start or the execute is sent, the abort stops only the wait, and a started run goes on, by its id.
|
|
76
76
|
|