@pipelex/sdk 0.33.0 → 0.34.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 +20 -1
- package/dist/cli/args.d.ts +41 -0
- package/dist/cli/args.js +96 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/bundle.d.ts +45 -0
- package/dist/cli/bundle.js +148 -0
- package/dist/cli/bundle.js.map +1 -0
- package/dist/cli/environment.d.ts +36 -0
- package/dist/cli/environment.js +72 -0
- package/dist/cli/environment.js.map +1 -0
- package/dist/cli/help.d.ts +4 -0
- package/dist/cli/help.js +56 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/inputs.d.ts +13 -0
- package/dist/cli/inputs.js +70 -0
- package/dist/cli/inputs.js.map +1 -0
- package/dist/cli/io.d.ts +79 -0
- package/dist/cli/io.js +96 -0
- package/dist/cli/io.js.map +1 -0
- package/dist/cli/main.d.ts +18 -0
- package/dist/cli/main.js +58 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/method.d.ts +56 -0
- package/dist/cli/method.js +108 -0
- package/dist/cli/method.js.map +1 -0
- package/dist/cli/present.d.ts +43 -0
- package/dist/cli/present.js +191 -0
- package/dist/cli/present.js.map +1 -0
- package/dist/cli/run.d.ts +17 -0
- package/dist/cli/run.js +290 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/script.d.ts +23 -0
- package/dist/cli/script.js +212 -0
- package/dist/cli/script.js.map +1 -0
- package/dist/cli/source.d.ts +49 -0
- package/dist/cli/source.js +71 -0
- package/dist/cli/source.js.map +1 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +65 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +49 -7
- package/dist/client.js +152 -26
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +42 -4
- package/dist/errors.js +43 -4
- package/dist/errors.js.map +1 -1
- package/dist/hooks/validate-client.d.ts +3 -3
- package/dist/hooks/validate-client.js +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/prepare-inputs.d.ts +6 -0
- package/dist/prepare-inputs.js +21 -10
- package/dist/prepare-inputs.js.map +1 -1
- package/dist/runs.d.ts +28 -1
- package/dist/runs.js +2 -1
- package/dist/runs.js.map +1 -1
- package/dist/upload.d.ts +6 -0
- package/dist/upload.js +9 -4
- package/dist/upload.js.map +1 -1
- package/dist/user-agent.d.ts +3 -2
- package/dist/user-agent.js +3 -2
- package/dist/user-agent.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +8 -3
- package/docs/cli.md +123 -0
- package/docs/client-identification.md +1 -1
- package/docs/errors.md +5 -3
- package/docs/input-preparation.md +6 -2
- package/docs/run-results.md +20 -0
- package/package.json +5 -1
package/dist/user-agent.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-agent.js","sourceRoot":"","sources":["../src/user-agent.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"user-agent.js","sourceRoot":"","sources":["../src/user-agent.ts"],"names":[],"mappings":"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"}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/docs/architecture.md
CHANGED
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
|
|
33
33
|
The package is ESM-only (`"type": "module"`, `dist/index.js` built by `tsc` with `module: NodeNext`) — there is no separate CJS build. CJS consumers can still load it: the `exports` map declares a `default` condition alongside `import`, both pointing at the same ESM `dist/index.js`, so `require("@pipelex/sdk")` resolves through Node's `require(ESM)` support rather than a transpiled dual build. That support needs Node 20.19+ or 22.12+ (unflagged); `engines.node` is pinned to `>=22.12.0` to match the workspace's Node 22 floor. This only works because `dist/` has no top-level await — if that ever changes, `require()` of the module will throw and a real dual ESM+CJS build (or a `require`-only CJS entrypoint) would be needed instead.
|
|
34
34
|
|
|
35
|
+
The package also declares one `bin`, `pipelex-sdk`, which is `dist/cli.js` with a `#!/usr/bin/env node` first line, so that `npx @pipelex/sdk` runs it ([`cli.md`](cli.md)). It is not in the `exports` map, so no consumer can import it, and it uses no top-level `await` either. `make test-pack` packs the package and runs the command from the tarball through `npx`, from a directory outside the package, which is how CI proves the published artifact exposes it.
|
|
36
|
+
|
|
35
37
|
Condition order in `exports` matters: `types` is listed first (TS/Node resolution convention — the type-checker looks at the first matching condition), then `import`, then `default` as the CJS fallback.
|
|
36
38
|
|
|
37
39
|
The map has a second entry, **`@pipelex/sdk/upload`** (`dist/upload-grant.js`), the browser-safe one. The main entry is Node-first — `upload.ts` names `node:fs/promises` and `artifacts.ts` names `undici`, so a browser-targeting bundler must mark them external — because the client holds an API key and belongs on a server. The subpath carries only what a page needs to store a file with an upload grant: `uploadWithGrant`, the grant types and the three error classes it raises. Its runtime import graph is `errors.ts` (with the fallback table it reads, `error-verdicts.ts`), `timers.ts` and `mthds/protocol`, nothing else, and the unit suite bundles it for the browser with esbuild, nothing marked external, so a Node-only module becoming reachable fails a test rather than a consumer's build. Both entries load the same `dist/errors.js` and `dist/upload-grant.js`, so the function and the error classes are identical whichever entry a consumer imports them through. Whether the main entry should become browser-safe as a whole is a separate question, because it touches how path strings are read.
|
|
@@ -96,9 +98,10 @@ Flat `src/` (mirrors `mthds-js`'s `runners/api` flatness — the SDK has one cli
|
|
|
96
98
|
- `execute-result.ts` — `PipelexExecuteResult`, a `DictRunResultExecute` subtype that adds a resolved `.main_stuff` accessor (dug out of `pipe_output`'s working memory via the response's `main_stuff_name`, throwing `MissingMainStuffError` if unlocatable). `execute()` returns it, giving blocking and durable results the **same output accessor** — `result.main_stuff` — so callers never branch on which path ran. The neutral `mthds` wire model leaves `main_stuff_name` in its extension index signature; this Pipelex-branded subtype declares it as a typed field (Pipelex owns that concept). `resultsFromExecute` (the public lift from that result onto `RunResults`, in the same module) reads that accessor too, keeping the resolution single-sourced; the `executeBlocking` fallback is its only caller inside the SDK, and a consumer driving `execute()` directly calls it by name.
|
|
97
99
|
- `errors.ts` — the error verdict and the typed errors. The verdict is `ErrorDomain` (`input`, `config`, `runtime`: who can fix the failure) and `ErrorVerdict` (that domain and `retryable`), carried as two readonly own properties by the abstract `PipelexRequestError`, which refines the protocol-base `PipelineRequestError` (re-exported from `mthds/protocol`) and takes the verdict as a constructor argument, so every subclass declares its verdict where it is defined and one that forgets it does not compile; `errorVerdictOf` reads the pair structurally from anything a `catch` holds. Under that base sit `RequestArgumentError` (the client refusing a call's arguments before sending any request, `input` unless the site declares otherwise, as the invalid base URL declares `config`), the transport and run-lifecycle errors, `PagingNotTerminatingError`, the input-preparation family under `InputPreparationError`, and the artifact family under `ArtifactOperationError` (`ScopeUnavailableError`, `ArtifactFetchError`, `ArtifactAuthenticationError`, the last carrying the download verdict so far); both family bases take an optional verdict for the sites, and the consumer subclasses, whose verdict is not the family's. `ApiResponseError` carries every member of the problem document it was built from, its nested members checked one level down by the readers a stored report goes through, and also stands for a 2xx answer the SDK could not read — the branch fields `errorDomain` and `type`, `retryable`, `userAction`, `requestId` (the body's, else the `X-Request-ID` header's), the native `code` and `errorType`, the platform's `errors[]`, the inference members, and the decoded document whole as `problemDocument` — and resolves `retryable` and `errorDomain` from the document's values when they are valid and the fallback's otherwise, except that a valid `input` or `config` domain sent without `retryable` is not retryable. Its member names match `mthds`'s own `ApiResponseError` wherever the two overlap, and so do their types except the verdict, which this SDK narrows to always decided; a type-level test pins the shared `ProblemDetails` and `UserAction` because this package may not import `mthds/errors`. `RunFailedError` carries the run's `status` and its stored report as `error`, and takes its verdict from that report. It is not the whole error surface: `codegen-check.ts` exports `CodegenLockError`, which extends plain `Error` because nothing was requested over the wire, and declares the two verdict members itself. See [errors.md](./errors.md#the-verdict-every-error-carries).
|
|
98
100
|
- `error-verdicts.ts` — `fallbackVerdict`, the table an `ApiResponseError` falls back to for a member the server did not send: the HTTP status, the platform `code`, and whether the body is named, which decides a `404`. Internal and pure. `pipelex-sdk` (Python) must apply the same table: `tests/fixtures/error-verdicts.json`, the case file this package's suite drives, is the contract, and the Python suite is to drive its own code through an identical copy of it once that copy is added.
|
|
99
|
-
- `user-agent.ts` — client identification per the
|
|
101
|
+
- `user-agent.ts` — client identification per the spec `conformance/specs/client-identification.md`, in the `conformance` repository beside the specs' tests: the exported `AppInfo` option, its RFC 9110 token validation (a `TypeError` on a bad field, a `RangeError` past the 512-character ceiling), its rendering, the runtime detection (Node, Bun, Deno, or a browser, where no header is set) and `buildUserAgent`, which the client calls once at construction. The barrel re-exports `buildUserAgent`, `validateAppInfo`, `MAX_USER_AGENT_LENGTH` and the `RuntimeInfo` type, so a consumer's raw fetches send the same value. The client's private `requestHeaders` is the one place API request headers are built, so every route carries the value; the artifact fetch of a presigned link never goes through it. See [client-identification.md](./client-identification.md).
|
|
100
102
|
- `timers.ts` — `MAX_TIMER_DELAY_MS`, the longest delay `setTimeout` honours, and `isTimerDelay`, which every caller-supplied `timeoutMs` is checked against before it reaches a timer: a longer delay overflows and fires at once. It imports nothing, so the browser-safe upload entry uses it too.
|
|
101
103
|
- `version.ts` — `SDK_VERSION`, in its own module so the client stamps it on the `User-Agent` without importing the barrel, which re-exports it.
|
|
104
|
+
- `cli.ts` and `cli/` — the `pipelex-sdk` command, `run` and `script` ([`cli.md`](cli.md)): `cli.ts` is the executable `package.json` declares as the package's one `bin`, handing the process to `cli/main.ts`'s `runCommand`, and `cli/` holds the flags, the bundle reader, the inputs, the environment, the two subcommands and what they print. It sits at the TOP of the graph, the other way round from every other module: nothing imports it, the barrel does not export it, and it reaches the SDK only through `index.ts`, which `dependency-cruiser` enforces in both directions.
|
|
102
105
|
- `index.ts` — the public barrel: `export * from "mthds/protocol"` (single import source for the standard surface) plus the client, models, run types, the standalone pure helpers (`runCodegenCheck`, `summarizeUsage`, `locateArtifacts`, `collectArtifacts`, `artifactFilename`), and errors.
|
|
103
106
|
|
|
104
107
|
## Client surface (current)
|
|
@@ -110,12 +113,12 @@ Flat `src/` (mirrors `mthds-js`'s `runners/api` flatness — the SDK has one cli
|
|
|
110
113
|
- **Method selectors on `validate`:** the first argument is inline contents (`string[]`) or a `ValidateMethodSelector` — `{ method_ref }` (runner-resolved by address) or `{ method_id }` (hosted, platform-resolved) — under the strict tooling XOR: exactly one selector, no linkage exception, `mthds_sources` legal only beside inline contents. A selector-resolution failure is a non-2xx `ApiResponseError`, never an `is_valid: false` verdict.
|
|
111
114
|
- **Crate routes (`/v1/resolve`, `/v1/codegen`, `/v1/pipe-io`):** `resolve` emits the **normalized library crate** (the MTHDS standard's Library Crate Format, fingerprint riding inside the payload); `codegen` projects that crate through two explicit axes — `kind` (`types`) × `target` (`ts-zod` / `python-pydantic` / `python-structures`) — into stamped artifacts plus their `codegen.lock`. All three share one closure envelope (`files: [{content, source?}]` XOR a `method_ref`, the address form server-resolved and the registry form a `501`) and return a **discriminated 200 verdict** (`is_valid`, with `CrateInvalidReport` as the shared invalid arm) rather than a bare payload, the same discipline as `validate`. They also take the hosted `method_id` (`PipelexHostedToolingExtensions`) as a third closure form under the strict three-way XOR — a pass-through the platform resolves, meaningless against a bare runner. Writing the artifacts and lock verbatim reproduces a local `pipelex codegen types` run byte-for-byte, so the offline `pipelex codegen check` passes — the SDK stays transport-only and does not write files. Verifying such a tree afterwards is `runCodegenCheck`, a standalone pure helper rather than a client method, because it needs no server at all. `pipeIo` derives a method's three I/O artifacts — the standard's `PipeIOContracts`, `InputForm` and `OutputForm`, imported from `mthds/protocol` — for the pipe the route selects (the request's qualified `pipe_ref`, else the method's own entry pipe) or for every pipe under `all_pipes`, with the runnability facts beside them and the closure's files echoed on `include_files`, and it runs no dry run; a selection it refuses is a `422` typed `EntryPipeNotFoundError` or `EntryPipeAmbiguousError`. See [crate-routes.md](./crate-routes.md).
|
|
112
115
|
- **Tools (`/v1/lint`, `/v1/format`):** `lint`, `format` — single-file static diagnostics and canonical formatting, served by any pipelex-api runner (no inference, no bundle load). Both are **diagnostic endpoints** like `validate`: a malformed `.mthds` file is a produced verdict on a **200** carrying `diagnostics[]`, never a thrown error — `format` additionally echoes the content back unchanged with `changed: false`. Only a no-verdict condition is non-2xx: a malformed body, **malformed formatter `options`** (e.g. a non-numeric `column_width` ⇒ 422), auth, or a server fault, all mapped to the typed `ApiResponseError`. `lint` is the cheap static check (syntax / schema / semantic); it does not replace `validate`, which loads the bundle, resolves across files, and dry-runs the pipes.
|
|
113
|
-
- **Durable run lifecycle (hosted extension — NOT protocol):** `getRunStatus`, `getRunResult` (with an optional `artifacts` selection sent as `?artifacts=`, see [run-results.md](./run-results.md#reading-only-some-artifacts--artifacts)), `waitForResult`, `startAndWaitForResult` (handshakes `/v1/version`, takes the durable start+poll path on a hosted deployment, and self-heals to the blocking `execute` against a bare runner). A run that ended without completing answers the results read with a `409` whose `run_status` and `error` members carry its status and its stored error report; `getRunResult` turns it into the `failed` arm (`status`, `message` from `detail`, `error`) and the waiting calls on the durable path into a `RunFailedError` carrying the same (the blocking fallback's failure is the runner's `ApiResponseError`), the status read from the member, or from the `detail` sentence on a platform that does not send it yet. The status read serves the same report as `RunRead.error`. See [errors.md](./errors.md#a-failed-run--runerrorreport). Reading what a completed run consumed is `summarizeUsage(results)`, a standalone pure helper rather than a client method, because it folds the usage pair the results already carry and needs no server. See [run-usage.md](./run-usage.md#summarizing-a-run--summarizeusage).
|
|
116
|
+
- **Durable run lifecycle (hosted extension — NOT protocol):** `getRunStatus`, `getRunResult` (with an optional `artifacts` selection sent as `?artifacts=`, see [run-results.md](./run-results.md#reading-only-some-artifacts--artifacts)), `waitForResult`, `startAndWaitForResult` (handshakes `/v1/version`, takes the durable start+poll path on a hosted deployment, and self-heals to the blocking `execute` against a bare runner). The handshake's outcome is cached for the client's lifetime, under the rule the Python SDK follows too: an answer that is not a usable version assumes hosted and lets the start surface the real error, while no answer at all throws its `ApiUnreachableError` at once, caches nothing and sends no start. An answer is anything the server sent back: an error status, a body that is not JSON, not UTF-8 or no version, and a body that arrived but could not be decoded, such as a broken gzip stream, which fetch fails with the decompressor's code (`Z_DATA_ERROR` and the other zlib codes, brotli's `ERR__ERROR_…`, zstd's `ZSTD_error_…`). A run that ended without completing answers the results read with a `409` whose `run_status` and `error` members carry its status and its stored error report; `getRunResult` turns it into the `failed` arm (`status`, `message` from `detail`, `error`) and the waiting calls on the durable path into a `RunFailedError` carrying the same (the blocking fallback's failure is the runner's `ApiResponseError`), the status read from the member, or from the `detail` sentence on a platform that does not send it yet. The status read serves the same report as `RunRead.error`. See [errors.md](./errors.md#a-failed-run--runerrorreport). Reading what a completed run consumed is `summarizeUsage(results)`, a standalone pure helper rather than a client method, because it folds the usage pair the results already carry and needs no server. See [run-usage.md](./run-usage.md#summarizing-a-run--summarizeusage).
|
|
114
117
|
- **Health:** `health` (origin-level `/health`), on the same transport as every route: an unreachable origin is an `ApiUnreachableError` and a non-2xx answer an `ApiResponseError` naming `/health`, whose verdict the fallback reads from the status. A 2xx whose body is not a JSON object is an `ApiResponseError` too, as on every protocol, extension and run-lifecycle route.
|
|
115
118
|
|
|
116
119
|
### Product routes (hosted management surface)
|
|
117
120
|
|
|
118
|
-
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
|
|
121
|
+
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).
|
|
119
122
|
|
|
120
123
|
- **User profile:** `getMe` (`GET /v1/me`).
|
|
121
124
|
- **Methods catalog:** `listMethods`, `getMethod`, `createMethod`, `updateMethod` (rename = changed `name`), `deleteMethod`. **`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 conflict` rather than a second cascade over the same runs, and an unknown or foreign-org id is a `404`. (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`, and a server-derived `description` (parsed read-side from the bundle's top-level `description`) 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 stored method's polymorphic `mthds` source into a runnable, provenance-labelled closure — see [input-preparation.md](./input-preparation.md#getmethodclosure--the-explicit-expansion-utility).
|
|
@@ -135,4 +138,6 @@ The hosted catalog/account routes the webapp drives. All ride the same `{base}/v
|
|
|
135
138
|
Two layers, split by whether a server is in the loop:
|
|
136
139
|
|
|
137
140
|
- **Unit suites (`tests/*.test.ts`, `make test`):** mock the fetch boundary (`vi.spyOn(globalThis, "fetch")`) and pin the client's wire behavior — URLs, request bodies, status→error mapping, poll semantics. These run everywhere, including CI. The codegen-check suite is the exception that proves the rule: it has no server to mock, so it runs against **vendored real codegen output** under `tests/fixtures/codegen/` — artifacts and lock emitted by an actual `pipelex codegen types` run, committed beside the `.mthds` they came from. A hand-written stamp would only prove the port agrees with itself. The artifacts are vendored with a trailing `.txt` and their real names restored at load time, because `format:check`, `lint`, and `typecheck:test` all glob `tests/**/*.ts` — and a single reformatted byte would invalidate the very hash the fixture exists to pin.
|
|
141
|
+
- **The command's case table (`tests/cli.test.ts`):** runs every case of `tests/fixtures/cli-cases.json` through the `pipelex-sdk` command in-process, in a fresh working directory, with `fetch` answering from the case's recorded routes, so the command runs on the real client. The table is written for the Python SDK's command of the same name to run too; [`cli.md`](cli.md) describes the format.
|
|
142
|
+
- **The packed command (`make test-pack`):** packs the package with `npm pack` and runs `pipelex-sdk --help` and `--version` from the tarball through `npx`, from a directory outside the package. It needs the registry for the dependencies, so CI's `js (make all)` job runs it and `make test` does not.
|
|
138
143
|
- **E2E suites (`tests/e2e/*.e2e.ts`, `make test-e2e`):** no mocks — they drive a **live pipelex-api server** (`PIPELEX_E2E_BASE_URL`, default `http://localhost:8081`) and verify the diagnostic-endpoint contract end-to-end with correct and malformed MTHDS bundles: clean content lints with zero diagnostics and formats to itself (idempotently), malformed content comes back as a 200 verdict (`diagnostics[]` on lint; `diagnostics[]` plus the content echoed back unchanged with `changed: false` on format; `is_valid: false` + `validation_errors[]` on validate), and malformed formatter options surface as a 422 `ApiResponseError`. The crate suite adds the case only a live server can settle: the `kind` / `target` vocabulary is a mirror of a Python `StrEnum` in another repo, so nothing but a real call proves the two still agree — and it cross-checks that `codegen`'s `crate_fingerprint` equals the `fingerprint` `resolve` returns for the same closure. It then runs `runCodegenCheck` over the artifacts and lock the server just emitted — verbatim first (current, with the lock header's fingerprint matching the response's), then mutated in memory once per drift category, for a `.ts` target and a `.py` one. The unit suite pins the port against vendored bytes; only this pins it against the stamp and lock bytes the server writes **today**, which is the pairing that matters when a consumer commits the server's artifacts and gates CI on the SDK's verdict about them. The same suite drives `pipeIo` across all three selectors: the artifacts under the resolved ref, their equality with `validate`'s views for the same closure, the `include_files` echo, `all_pipes` on a method with no entry pipe, a `method_ref` package's manifest entry pipe, the invalid arm, and the typed selection `422`. Its `method_id` case, like the `prepareInputs` suite's, runs only when `PIPELEX_E2E_METHOD_ID` names a stored method the key's organization owns, since only a hosted origin can resolve one, and is skipped otherwise. The distinct `.e2e.ts` suffix + `vitest.e2e.config.ts` keep them out of the default `vitest run`, so `make test` / CI never require a server. An unreachable server is an error, never a silent skip, and it is reported once: `make test-e2e` preflights the target with a single `curl` and bails with a one-line message, instead of letting every suite fail in `beforeAll` and print a socket-error dump each. The per-suite `beforeAll` guard stays as the backstop for a direct `npm run test:e2e` run. Both probe **`/v1/version`**, deliberately: a bare runner serves the origin-level `/health` and a hosted origin does not (it answers `/v1/health` instead), so `/v1/version` is the one unauthenticated route both answer — probing anything else reports a perfectly reachable hosted origin as down. The target itself is resolved once, in the Makefile, and exported along with `PIPELEX_API_KEY`, so the URL the preflight checks is always the URL the suites call: the shell environment (or a `make test-e2e PIPELEX_E2E_BASE_URL=...` override) wins, then `.env`, then `http://localhost:8081`. The run echoes the resolved target before it starts, since `.env` can now supply it invisibly. Note that a hosted origin cannot run the whole suite today — `lint` and `format` are not exposed there (gateway `403`), while the protocol routes and the `resolve` and `codegen` crate routes answer, and `pipe-io` answers only once the platform's proxy and the gateway list it — so a fully green run means a local runner. The upload-grant suite needs the same hosted platform: it requests a grant, sends a small PDF with `uploadWithGrant`, reads the bytes back equal through `fetchArtifact`, and proves the refusals the error mapping reads — the `412` of a used grant, the `403 SignatureDoesNotMatch` of a file one byte longer than declared, and the `413` of a declared size over the cap. It runs in Node, whose `fetch` sends no CORS preflight, so the bucket's CORS rule is proven by a page, not by it. The artifact suite is the mirror image: it uploads a file with `prepareInputs`, runs a pass-through and brings the file back down with `downloadArtifacts` over `working_memory`, bytes equal, so it needs a hosted platform carrying `POST /v1/pipe-io` (which `prepareInputs` reads), the upload, run-lifecycle and bulk resolve routes, and a key for it, and it fails honestly rather than skipping against a bare runner.
|
package/docs/cli.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# The `pipelex-sdk` command
|
|
2
|
+
|
|
3
|
+
`@pipelex/sdk` publishes one command, `pipelex-sdk`, which runs a method on the Pipelex API with nothing to install but Node 22.12 or later. Its subcommands are `run`, which runs a method and prints its main output as JSON, and `script`, which writes a short shell script that runs one method with this SDK's version pinned, which is the lightest way to give a method a command of its own.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @pipelex/sdk run --method github.com/acme/methods/receipt-review@v1.0.0 --inputs inputs.json
|
|
7
|
+
npx @pipelex/sdk run --method ./receipt-review --inputs-template > inputs.json
|
|
8
|
+
npx @pipelex/sdk script --method github.com/acme/methods/receipt-review@v1.0.0 --pipe receipts.review_receipt
|
|
9
|
+
./receipt-review --inputs inputs.json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`npx` runs a package's only command whatever its name, so `npx @pipelex/sdk …` is enough. In a project that has the SDK installed, the command is `npx pipelex-sdk …`. The Python SDK, `pipelex-sdk` on PyPI, publishes the same command under the same name (`uvx pipelex-sdk …`, its `docs/cli.md`), with the same flags and the same output, held to this one by the recorded case table described at the end of this page.
|
|
13
|
+
|
|
14
|
+
The command lives in `src/cli.ts`, the executable, and `src/cli/`. It sits at the top of the SDK's dependency graph: nothing in the SDK imports it, the package entry does not export it, and it reaches the SDK only through the public barrel, as any other caller would (`.dependency-cruiser.cjs` holds both rules). It adds no runtime dependency: its flags are read with `node:util`'s `parseArgs`.
|
|
15
|
+
|
|
16
|
+
## `run`
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
pipelex-sdk run --method <method> [--pipe <domain.pipe_code>] [--inputs <file> | --inputs-template]
|
|
20
|
+
```
|
|
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.
|
|
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
|
+
- **`--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
|
+
- **`--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`.
|
|
26
|
+
|
|
27
|
+
The run goes through the SDK's `startAndWaitForResult`: on the hosted API a durable start, then polls until the run ends, however long it takes, and on a bare runner the blocking execute. Its `onStarted` callback prints `Run started: <id>` on stderr as soon as the run exists ([`run-results.md`](run-results.md)). The poll reads only the `main_stuff` artifact.
|
|
28
|
+
|
|
29
|
+
**stdout carries the run's main output and nothing else**, as indented JSON (two spaces), followed by a newline: exactly what `JSON.stringify(value, null, 2)` prints, non-ASCII characters as they are. It is printed as the method produced it, unvalidated and unrepaired: a list output arrives either bare or wrapped as `{"items": […]}` depending on the runtime path, and is printed as it arrived. An output the method left absent, which the run delivers as the runtime's absence document (matched by its whole shape, as `cli-python` matches it) or as nothing, prints `null`, and stderr says `The method left its output "<name>" absent (<reason>), so the result is null.` A produced file is not downloaded: its short-lived signed URL is in the printed JSON. A reader that stops early and closes either stream, as `| head` or `2>&1 | head -1` does, ends nothing: the lines it no longer reads are dropped, and the command goes on waiting for the run it started and exits as it would have.
|
|
30
|
+
|
|
31
|
+
## `script`
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
pipelex-sdk script --method <address | catalog id> [--pipe <domain.pipe_code>] [--name <name>] [--dir <dir>]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`script` checks the method with one `POST /v1/pipe-io` call, which selects the pipe as the run will and spends no inference, so a typo in the address or the pipe is caught when the script is written rather than when it first runs. Then it writes an executable file and prints its path on stdout:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
#!/bin/sh
|
|
41
|
+
# receipt-review: runs github.com/acme/methods/receipt-review@v1.0.0 on the Pipelex API.
|
|
42
|
+
# Written by @pipelex/sdk X.Y.Z. Needs Node 22.12 or later, and PIPELEX_API_KEY in the environment.
|
|
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' "$@"
|
|
45
|
+
```
|
|
46
|
+
|
|
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}`) 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
|
+
- **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.
|
|
51
|
+
- **On Windows** the script needs a POSIX shell, such as Git Bash or WSL; otherwise type the `npx` line directly.
|
|
52
|
+
|
|
53
|
+
## The environment
|
|
54
|
+
|
|
55
|
+
The key comes from `PIPELEX_API_KEY` and nowhere else, since a flag would put it in the shell's history and the process list; its absence is a usage error that says where to get a key, and the command never prints it. The API's address comes from `PIPELEX_BASE_URL`, `https://api.pipelex.com` when unset; a refused base URL is reported without its credentials, path or query. An empty variable counts as unset, except `PIPELEX_BASE_URL`: an empty base URL is refused like any other the SDK cannot use, since it is most often a variable that was meant to name another API, as in `PIPELEX_BASE_URL=$STAGING_URL` with `STAGING_URL` unset, and counting it as unset would send the key to the default address. Every request carries `pipelex-sdk-cli/<version>` before the SDK's own token in its `User-Agent` ([`client-identification.md`](client-identification.md)).
|
|
56
|
+
|
|
57
|
+
**No `.env` file is read.** A command started through `npx` from wherever the person stands would otherwise pick up a credential from whichever file it found there. To load one into the shell: `set -a; . ./.env; set +a`.
|
|
58
|
+
|
|
59
|
+
**`PIPELEX_SDK_POLL_INTERVAL_MS` is for tests only.** It sets the poll interval in milliseconds, a whole number; the case table sets it to `0` so that a run answered "still running" is polled again at once, and serves its `202` answers with `Retry-After: 0`, since the SDK waits at least as long as the server's `Retry-After` and five seconds when a `202` gives none. Unset, the SDK's own interval applies. The Python command reads the same variable.
|
|
60
|
+
|
|
61
|
+
## Exit codes, errors and Ctrl-C
|
|
62
|
+
|
|
63
|
+
The exit code is presentation, following the workspace's surface-output rule: a script reads stdout.
|
|
64
|
+
|
|
65
|
+
| Code | When |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `0` | The result was printed, or the script written, or the help or version shown |
|
|
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 |
|
|
70
|
+
| `130` | Interrupted: on Linux and macOS the process ends by the interrupt signal, which a shell reports as `130` |
|
|
71
|
+
|
|
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:
|
|
73
|
+
|
|
74
|
+
- **A refused request**: `Error: the API refused the request (status <n>).`, then `Reason: <the problem's detail>`, each validation error as ` - <file>: <message>`, `Next step: <the advised action>` and `Request id: <id>` when the answer carries them.
|
|
75
|
+
- **A failed run**: `Error: run <id> ended with status <STATUS>.`, then `Reason: <the report's message>` (the problem's own sentence when the run has no report) and `Next step: <the report's advised action>`.
|
|
76
|
+
- **A method that does not load**: `Error: the method does not load.`, then one line per validation error that carries a string category and message, as ` - <file>: <message>`, or, when there is none, ` - <the answer's own message>`. It reads the same whichever request found it: the pipe I/O call of `--inputs-template` and `script`, or the input preparation of a run with inputs.
|
|
77
|
+
- **A value at a file input that is no file**: `Error: the inputs hold a value at a file input that cannot be read as a file.`, then `Reason: <the SDK's message>` and the forms a file input takes. An unreadable local path reads `Error: Local file cannot be read: "<path>" (<reason>).`
|
|
78
|
+
- **An unreachable API**: `Error: could not reach the Pipelex API at <base URL> (<code>).`
|
|
79
|
+
- **A failed upload** of a file the inputs name: `Error: the upload of "<file>" failed (status <n>).`, the status left out when no answer came back, then `Reason: <the SDK's message>`, which says what the status means for an upload (a file past the size limit, a deployment without file upload, a credential refused), and `Next step:` and `Request id:` when the API's answer carries them. It covers the SDK's `RejectedAssetError`, `UploadAuthenticationError`, `UnsupportedUploadCapabilityError` and `UploadTransportError`, each of which names the file it was sending. It exits `1`, and no run starts.
|
|
80
|
+
- **A wait that fails once the run exists**, on an unreachable API, a refused poll or an answer the command cannot read: the failure's own lines, then `Run <id> may still be going on the server, so do not start it again.`, since the run was started and nothing says it ended. A run that ended in failure (`RunFailedError`) and a run that ended without its main output (`MissingMainStuffError`) get no such line. It exits `1`.
|
|
81
|
+
- **A start whose outcome is unknown**: once a request that may create a run has left, the start or a bare runner's blocking execute, and before the API names the run, a failure that leaves it unknown whether a run was created adds `A run may have started on the server without the command learning of it, so check before starting it again.` under its own lines. That is an answer lost to a time limit or to a connection that closed once the request had left, a request the gateway cut off, a gateway's `502` or `504`, which says it lost or never got the server's answer (RFC 9110), and an answer the command cannot read. The hosted gateway cuts off a request it waits on at about 30 seconds and answers `503` or `504`, while the server may still be handling it: a blocking execute, or a start whose server is fetching a `method_ref`'s package. The command times the request from the moment the SDK's `onStarting` announces it, and the SDK's `isGatewayCutOff` reads a `503` that came back past its threshold, about 28 seconds, as that cut-off. Any other answer from the API, a `503` that came back sooner, saying the request was not handled, included, and a transport failure that proves the request never left, add nothing, since no run was created. Such a failure names a step of the connection's set-up, which no request follows: an unknown host (`ENOTFOUND`, `EAI_AGAIN`), a refused connection (`ECONNREFUSED`, from every address the host has included), no route to the host or to the network (`EHOSTUNREACH`, `ENETUNREACH`, from the `connect` call), the connection given up by the system's or undici's own connect time limit (`ETIMEDOUT` from `connect`, `UND_ERR_CONNECT_TIMEOUT`), a server certificate the TLS handshake refused (`CERT_HAS_EXPIRED`, `DEPTH_ZERO_SELF_SIGNED_CERT`, `ERR_TLS_CERT_ALTNAME_INVALID` and the other verdicts of the certificate's check Node names, then `UNSPECIFIED`, its code for every verdict it does not name, such as an unhandled critical extension). Both commands read the same failures the same way, so where one command cannot tell whether the request had left, both take the reading that a run may exist: a connection dropped during the TLS handshake, which Node reports as the reset (`ECONNRESET`) of a connection dropped once the request had left, and another TLS failure. A host that drops packets is a connection that never comes, and each command meets its transport's connect time limit first, which proves nothing was sent: here undici's own, ten seconds for the TCP connect and the TLS handshake together (`UND_ERR_CONNECT_TIMEOUT`), which runs out before the SDK's own time limit (`ABORT_TIMEOUT`), at least 30 seconds on every request the command sends; in the Python command, httpx's connect timeout, which the one time limit the Python client passes bounds. Fetch cannot say whether the SDK's own time limit ran out before the connection was made or once the request had left, so the command reads it as an answer lost to a time limit; with Node's own dispatcher it is one, since undici gives up a connection that does not come sooner. It exits `1`.
|
|
82
|
+
|
|
83
|
+
The checks that need no request come first, so a mistake costs nothing. `run` checks, in this order, its flags, `--pipe`'s form, the method (reading a path from disk), the inputs, the poll interval, the key and the base URL. `script` checks its flags, `--pipe`'s form, a control character in any value, the method's form, `--name`, `--dir`, the target file when its name is already known, the key and the base URL. The flags are read in command-line order and the first problem is the one reported: `--help` or `-h` anywhere before the `--` that ends the options asks for the help whatever else is there, and an argument that is exactly one of the two is never taken as a flag's value, so `run --method --help` prints the help, and so does `run --pipe -- --help`, where the `--` is `--pipe`'s value and ends nothing; an unknown option or a positional argument is refused; a flag given twice is refused rather than the last one winning; a string flag needs a non-empty value, given as the next argument or after `=`, and a next argument starting with `-` is not taken as one, except `-` alone, which `--inputs` reads as stdin. Before a subcommand, `--help` prints the command's help whatever follows it, as a subcommand's does, while `--version` must stand alone: `pipelex-sdk --version run …` is a usage error, since printing the version would leave the rest of the command line unanswered.
|
|
84
|
+
|
|
85
|
+
**Ctrl-C does not stop the run.** The command stops waiting, says where the run stood and exits `130`: until a request that may create a run is about to leave, the version check included, `Interrupted. No run was started.`; from then until the API answers with the run's id, `Interrupted before the API answered with a run id. A run may or may not have started on the server.`; once the run exists, `Interrupted. Run <id> keeps going on the server.` The moment between the first two is the one the SDK announces through `startAndWaitForResult`'s `onStarting`, right before it sends the start or a bare runner's blocking execute ([`run-results.md`](run-results.md)). Fetching that run later is not built into the command: the SDK's `waitForResult(id)` does it. A second Ctrl-C ends the process at once.
|
|
86
|
+
|
|
87
|
+
**An interrupt sends nothing more.** Every request the command makes starts only if no interrupt has landed, so a Ctrl-C during the local work (reading the bundle or the inputs, a read from stdin or a named pipe included) sends no request at all, not the pipe I/O call, the version check nor the start, and the command says `Interrupted. No run was started.` A local read that never completes, such as `--inputs` naming a pipe no one writes to or a bundle on a stalled network mount, does not hold the command either: it stops waiting for the read. An upload under way when the interrupt lands finishes, but no further upload starts, one whose file was being read included, and neither does the run. `script` interrupted before it starts writing its file, during its last check that the name is free included, writes nothing and says `Interrupted. Nothing was written.`; once it has started, the write is not raced with the interrupt, which waits for the file to be whole, then says `Interrupted. ./<name> was written.`, so that the message is true wherever the interrupt lands. A second Ctrl-C ends the process at once, as it does anywhere in the command, printing nothing more; should it land between the file's creation and the end of its write, which on a local disk takes no time but can last where the write blocks, on a stalled mount for instance, that file stays behind, empty or partial, for the person to remove. The Python command stops the write instead and removes what it wrote. Node puts every signal back to its default action as it starts, so this command takes Ctrl-C even when it was started with SIGINT ignored, as a background job of a non-interactive shell is, where the Python command keeps it ignored; and `--inputs-template` says `Interrupted.`
|
|
88
|
+
|
|
89
|
+
**How the executable ends after Ctrl-C.** On Linux and macOS, once it has said where the run stood, the process ends by the interrupt signal itself, which a shell reports as status `130` and which tells a calling loop that the person interrupted. It does not call `exit(130)`, because Node's exit waits for its worker threads, and one can stay blocked for good in a read the command stopped waiting for. On Windows it exits with `130`. `runCommand`, the function the executable calls, returns `130` in every case.
|
|
90
|
+
|
|
91
|
+
## Reading a bundle
|
|
92
|
+
|
|
93
|
+
A `--method` path is read by the command's own reader (`src/cli/bundle.ts`), whose rules are recorded as cases so that the Python command reads a bundle the same way:
|
|
94
|
+
|
|
95
|
+
- **A `.mthds` file** is a one-file bundle, sent under its own file name. A file of any other name is refused.
|
|
96
|
+
- **A directory** is every `.mthds` file under it, at any depth, each sent under its path relative to the directory, with `/` between segments.
|
|
97
|
+
- **The order** is the order of those names compared code point by code point, which is the order of their UTF-8 bytes: `Zeta.mthds` before `main.mthds`, `steps/score.mthds` before `steps2.mthds`, and `~.mthds` (U+FF5E) before `🧾.mthds` (U+1F9FE), where UTF-16 order would put them the other way round. A run sends the contents in this order as `mthds_contents`, and the pipe I/O route gets them as `files`, each with its name as `source`.
|
|
98
|
+
- **A hidden entry**, a file or a directory whose name starts with `.`, is skipped, so a `.venv/` or a `.git/` inside a bundle is never read.
|
|
99
|
+
- **Any file not named `.mthds`** is ignored, Python files included: the command sends only the MTHDS sources.
|
|
100
|
+
- **A symbolic link** is judged by its own name. One named `.mthds` that leads to a file is read through, under the link's name; one that leads to a directory is not descended into, which keeps a link cycle from looping; one that leads nowhere is refused. The `--method` path itself may be a link, and is followed.
|
|
101
|
+
- **A directory with no `.mthds` file** under it is refused, and so is a file that is not UTF-8 text and a directory that cannot be listed. A byte-order mark is kept as the file's first character, as it is on disk.
|
|
102
|
+
|
|
103
|
+
## The case table
|
|
104
|
+
|
|
105
|
+
The JavaScript and Python commands share no code, so they are held together by one recorded table of cases written for both: `tests/fixtures/cli-cases.json` is its source, and `python/tests/fixtures/cli-cases.json` its copy, which `make shared-files` at the repository root writes and `make check-shared-files` holds byte for byte (`docs/ci.md` at the root). A behaviour of the command changes here, in the table and in `src/cli/`, and the Python command follows it. `tests/cli.test.ts` runs every case through `runCommand` in-process, and the Python SDK's `tests/unit/test_cli.py` runs every case through its own `run_command`; a case that uses a field a suite does not know fails rather than being skipped, so a case added for one language reaches the other.
|
|
106
|
+
|
|
107
|
+
The table holds:
|
|
108
|
+
|
|
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
|
+
- **`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}}`.
|
|
112
|
+
- **`cases`**, each with these fields:
|
|
113
|
+
- `name`, unique, and `summary`, one sentence saying what the case holds.
|
|
114
|
+
- `argv`, the arguments after the command's name.
|
|
115
|
+
- `env`, merged over the table's `env`; a `null` value unsets the variable. The command sees only this environment.
|
|
116
|
+
- `files`, created in a fresh, empty working directory before the command runs, parents included: `{path, text}`, `{path, base64}` for bytes that are not UTF-8, `{path, symlink}` whose value is the link's target as written, relative to the link's directory, and `{path, directory: true}`.
|
|
117
|
+
- `stdin`, the text stdin holds; empty when absent.
|
|
118
|
+
- `stdin_error`, the system error code reading stdin fails with, such as `EIO`, instead of the text.
|
|
119
|
+
- `routes`, keyed `"<METHOD> <path>[?<query>]"` on the base URL, each a list of exchanges answering that route's calls in order, each once. An exchange gives the answer's `status`, `headers` and JSON `body` (or raw `text`, or `base64`, raw bytes such as a body that is not UTF-8, or none), or `answer`, the name of a table-level answer whose fields it takes, or `unreachable`, a request that never got a connection, of a kind: `"refused"`, `"refused-every-address"` (a host with several addresses, each refusing), `"unknown-host"`, `"no-route"`, `"no-network"`, `"connect-timeout"` (the transport's own connect time limit, which a host that drops packets meets first: undici's ten seconds in Node, httpx's connect timeout in Python), `"system-connect-timeout"`, `"certificate"` (the server's, refused), `"certificate-unnamed"` (refused for a reason Node gives no code of its own), `"handshake-dropped"` (during the TLS handshake); or `lost`, a request sent whose answer never comes back: `"timeout"` when the time limit runs out first, `"closed"` when the connection closes first, `"reset"` when it is reset, `"no-route"` when it loses its route to the host. This suite throws each kind as Node's fetch reports it, read off the real library against local listeners and certificates, but for a missing route, which takes the shape every failed `connect` call takes: a system error with its `code`, `errno` and `syscall` under fetch's `TypeError` (`ECONNREFUSED` from `connect`, an `AggregateError` of those for every address, `ENOTFOUND` from `getaddrinfo`, `EHOSTUNREACH` and `ENETUNREACH` from `connect` but from `read` once lost, `ETIMEDOUT` from `connect`, `ECONNRESET` from `read` for a dropped handshake and a reset alike), undici's `UND_ERR_CONNECT_TIMEOUT` and `UND_ERR_SOCKET`, the certificate check's `CERT_HAS_EXPIRED` and `UNSPECIFIED`, and the client's own `TimeoutError` for a `timeout`. An answer with `Content-Encoding: gzip`, which the table records only broken, fails as undici fails it, while its body is read (`Z_DATA_ERROR`), since a fetch mock decodes nothing. `elapsed_ms`, on any exchange, is how long it takes to come back, as the clock the command and the SDK time a request with sees it: the suite moves that clock forward by as much while it serves the exchange, rather than waiting, which is how a case holds an answer that came back past the gateway's ~30-second cut-off. This suite runs `performance.now`, the monotonic clock both read, as it does, plus the time the case's exchanges have taken. `request_body`, when given, is what the command must send: the sent JSON body, without its top-level keys whose value is `null`, which mean absent, must equal it. Every request must carry `Authorization: Bearer <PIPELEX_API_KEY>`. A request the case did not record, and a recorded exchange the command never asked for, fail the case.
|
|
120
|
+
- `interrupt`, optional, `{route, call}`: when that call of that route arrives, the suite interrupts the command, as Ctrl-C would, instead of answering; the route lists no exchange for it.
|
|
121
|
+
- `expect`: `exit_code`; either `stdout`, exact, or `stdout_includes`, substrings that must appear in order; `stderr`, substrings that must appear in stderr in this order, each found after the end of the one before, so a case pins the command's own sentences and the data they carry without pinning every line an SDK adds; `stderr_excludes`, substrings that must not appear anywhere, such as a key or a password; `files`, each `{path, text, executable}` that must exist with exactly that text, and with its owner's execute bit when `executable`; and `absent_files`, paths that must not exist.
|
|
122
|
+
|
|
123
|
+
**Writing a case.** A number in a recorded answer is an integer below 2^53 or a decimal whose shortest form both languages print alike, never `1.0` nor `1e16`, and an object's keys are never integer-like, since JavaScript would print those first; the two commands then print a recorded output byte for byte alike. A needle that ends with `\n` pins a whole line's end. A failure only one runtime can meet or tell apart, such as httpx's pool timeout, has no kind in the table, and that runtime's own suite tests it. Add the case at the source, run `make shared-files` at the root, and make it pass in both suites.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Client identification (`User-Agent` and `appInfo`)
|
|
2
2
|
|
|
3
|
-
Every request `PipelexApiClient` sends to the Pipelex API carries a `User-Agent` header that says which program made it. The hosted platform parses that header into the client surface it reports in product analytics and in its access log, which is how it tells a run started by an SDK script from one started by the web app, the MCP server, the CLI or the coding-agent hook. The convention is shared by every first-party client and is fixed by the
|
|
3
|
+
Every request `PipelexApiClient` sends to the Pipelex API carries a `User-Agent` header that says which program made it. The hosted platform parses that header into the client surface it reports in product analytics and in its access log, which is how it tells a run started by an SDK script from one started by the web app, the MCP server, the CLI or the coding-agent hook. The convention is shared by every first-party client and is fixed by the spec `conformance/specs/client-identification.md`, in the `conformance` repository, where the cross-repo specs sit beside the tests that verify them; this page describes how this SDK implements it.
|
|
4
4
|
|
|
5
5
|
## What the SDK sends
|
|
6
6
|
|
package/docs/errors.md
CHANGED
|
@@ -32,6 +32,8 @@ try {
|
|
|
32
32
|
|
|
33
33
|
**"Retryable" means a retry can succeed, not that it is safe.** The verdict says whether asking again can plausibly succeed; it says nothing about whether the first attempt had an effect, and it does not depend on the route. A `start` answered with a `500` is retryable by its status, yet it may already have created a run, so a caller that must not start a run twice keeps its own rule for that until the SDK can send an idempotency key.
|
|
34
34
|
|
|
35
|
+
**A request the gateway cut off: `isGatewayCutOff`.** The hosted gateway gives up on a request it waits on at about 30 seconds and answers `503` or `504`, while the server may still be handling it: a `start` whose server is fetching a `method_ref`'s package, for one. `isGatewayCutOff(error, elapsedMs)` tells such a failure from a refusal: it is `true` when, at least ~28 seconds after the request was sent, the failure is a `503` or `504` answer or the client's own time limit (`ApiUnreachableError` with the code `ABORT_TIMEOUT`), and `false` before then, since a fast `503` is the API saying it did not handle the request, and for any other failure. The blocking `execute` applies it and throws `PipelineExecuteTimeoutError`; a caller timing its own start, from `startAndWaitForResult`'s `onStarting` for one, applies it to learn that a run may exist although the start failed, timing it on the monotonic clock `performance.now`, as the client does, since a change of the system's time moves `Date.now`. The fetch API cannot say whether `ABORT_TIMEOUT` ran out before the connection was made, so the rule reads it as the gateway's; with Node's own dispatcher it never does past the threshold, since undici gives up a connection that does not come after ten seconds (`UND_ERR_CONNECT_TIMEOUT`), the TLS handshake included. The Python SDK's `is_gateway_cut_off` is the same rule.
|
|
36
|
+
|
|
35
37
|
**What carries no verdict.** `errorVerdictOf` returns `undefined` for what is not an SDK failure: the `RangeError` or `TypeError` the SDK raises for an argument of the wrong type or range before any request is sent (a `timeoutMs` out of range, a malformed `appInfo`, an empty artifact selection), which names a bug in the calling code; and the caller's own abort, which the client rethrows untouched. The per-reference errors inside a `downloadArtifacts` verdict are values with a `code`, not thrown errors, and carry none either. An argument the client refuses for what it asks, such as two method selectors given to `validate` or `execute`, or a base URL with a path, is a `RequestArgumentError`, which carries a verdict like every SDK error.
|
|
36
38
|
|
|
37
39
|
### Each class's verdict
|
|
@@ -39,7 +41,7 @@ try {
|
|
|
39
41
|
| Class | `errorDomain` | `retryable` | Why |
|
|
40
42
|
|---|---|---|---|
|
|
41
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). |
|
|
42
|
-
| `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`. 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`. 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`. |
|
|
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`. 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`. |
|
|
43
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. |
|
|
44
46
|
| `PipelineExecuteTimeoutError` | `input` | no | The blocking `execute` cannot outlast the gateway's limit; start the run and poll instead. |
|
|
45
47
|
| `RunFailedError` | the report's | the report's | See [a failed run](#a-failed-run--runerrorreport). |
|
|
@@ -47,7 +49,7 @@ try {
|
|
|
47
49
|
| `RunLifecycleUnavailableError` | `config` | no | The base URL points at a bare runner without the run lifecycle. |
|
|
48
50
|
| `MissingMainStuffError` | `runtime` | no | The API broke its own contract on a completed run. |
|
|
49
51
|
| `PagingNotTerminatingError` | `runtime` | no | A server minting cursors forever; `pageLimit` is the ceiling `iterateMethods` stopped at. |
|
|
50
|
-
| `InputPreparationError` and `EmptyMethodSourceError`, `InvalidLocalSourceError`, `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. |
|
|
52
|
+
| `InputPreparationError` and `EmptyMethodSourceError`, `InvalidLocalSourceError`, `InvalidInputValueError`, `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. |
|
|
51
53
|
| `UnsupportedUploadCapabilityError`, `UploadAuthenticationError` | `config` | no | The deployment or the credential must change. |
|
|
52
54
|
| `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. |
|
|
53
55
|
| `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). |
|
|
@@ -79,7 +81,7 @@ On the platform today the fallback decides both members, since the platform does
|
|
|
79
81
|
|
|
80
82
|
**The verdicts are a contract with the Python SDK.** `pipelex-sdk` must apply the same fallback and declare the same verdict on each class it shares with this package. `tests/fixtures/error-verdicts.json` holds every case the two must agree on, the fallback's rows, the server-sent resolution and each class's variants, and this package's test suite drives its own code through it. The Python package is to keep a byte for byte identical copy of the file and drive its own code through the same cases, with a check at the repository's root holding the two copies identical; until that copy is added, nothing holds the Python SDK to the table yet.
|
|
81
83
|
|
|
82
|
-
The shapes are not this SDK's invention. The report is the runner's `ErrorReport` (in `pipelex`), stored and served by the platform exactly as the runner wrote it, and the problem members are the ones the
|
|
84
|
+
The shapes are not this SDK's invention. The report is the runner's `ErrorReport` (in `pipelex`), stored and served by the platform exactly as the runner wrote it, and the problem members are the ones the hosted-envelope spec names, `conformance/specs/pipelex-hosted-envelope.md` in the `conformance` repository, where the cross-repo specs sit beside the tests that verify them. The types live in `src/error-models.ts`; `pipelex-sdk` (Python) carries the same fields under snake_case names.
|
|
83
85
|
|
|
84
86
|
## A failed run — `RunErrorReport`
|
|
85
87
|
|
|
@@ -105,6 +105,8 @@ Takes the **method** as exactly one of three selectors, the optional target **pi
|
|
|
105
105
|
|
|
106
106
|
The prepared `inputs` are passed to the existing run lifecycle unchanged.
|
|
107
107
|
|
|
108
|
+
**Stopping it.** An optional `signal` stops the preparation between its steps: once it has aborted, neither the pipe I/O request nor any upload starts, and `prepareInputs` throws the abort, as the run wait does. It is read again once each file's bytes are in hand, right before its upload request, so a file whose read outlasted the caller's patience is not sent; `uploadFile` takes the same `signal` in its options for that check. A request already sent runs to its end, so an upload under way finishes, but the file after it is not sent. The signal is not sent to the server.
|
|
109
|
+
|
|
108
110
|
#### The three selectors
|
|
109
111
|
|
|
110
112
|
Exactly one per call. The type pins the other two to `never`, so a second selector is a compile error; an untyped caller gets an `InputPreparationError` naming the three forms. **Empty is absent** — `files: []`, `method_ref: ""`, `method_id: " "` — mirroring the run options' rule, so an empty selector may sit beside a real one without tripping the XOR.
|
|
@@ -222,15 +224,17 @@ Upload is a **hosted Pipelex-product capability**, even though the SDK can be po
|
|
|
222
224
|
|
|
223
225
|
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):
|
|
224
226
|
|
|
225
|
-
- **an unresolvable signature** (`InputPreparationError`) — a bare or alias-qualified `pipe_ref`, refused before any request; a closure that does not load (`
|
|
227
|
+
- **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;
|
|
226
228
|
- **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`;
|
|
229
|
+
- **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;
|
|
227
230
|
- **invalid local source** (`InvalidLocalSourceError`) — missing, unreadable, or a path string outside Node;
|
|
231
|
+
- **invalid input value** (`InvalidInputValueError`) — 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. 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;
|
|
228
232
|
- **rejected asset** (`RejectedAssetError`) — the server refused it (e.g. a `413` past the service-defined size cap — see "Storage policy" — surfaced as a clear rejection with `code` `too_large`, not a raw transport error), or storage refused an upload with a grant (a used or expired grant, or a file that differs from what the grant signed — see the table above for each `code`);
|
|
229
233
|
- **unsupported server capability** (`UnsupportedUploadCapabilityError`) — the configured deployment has no upload route;
|
|
230
234
|
- **authentication / authorization failure** (`UploadAuthenticationError`) — `401` / `403`;
|
|
231
235
|
- **transport failure** (`UploadTransportError`) — a network or server fault, or any other unexpected upload failure; its `status` is the HTTP status when a response produced it, and its `code` says which failure it was. From `uploadFile` the code is `server_error` for a `5xx`, `timeout` when the client's own request timeout ran out, `unreachable` when no response came back, and `unexpected` for any other status or failure, a `2xx` answer the SDK cannot read included, such as one with no string `uri`; `uploadWithGrant`'s codes are in the table above.
|
|
232
236
|
|
|
233
|
-
|
|
237
|
+
**Which file.** Every upload failure `uploadFile` raises names the file it was sending, as it sent it: `RejectedAssetError`, `UploadAuthenticationError`, `UnsupportedUploadCapabilityError` and `UploadTransportError` each carry `filename`, so a consumer that prepared several files says which one failed without parsing a message. `uploadWithGrant` sends a grant's file, not a named one, and leaves `filename` undefined on the `UploadTransportError` it raises.
|
|
234
238
|
|
|
235
239
|
Each of these carries the verdict every SDK error does, `retryable` and `errorDomain` ([errors.md](./errors.md#each-classs-verdict)): the family's own is `input` and not retryable, an unsupported deployment and a refused credential are `config`, and a transport failure takes the verdict of the error it wraps, so a `402` plan refusal wrapped by `uploadFile` is `config` and not retryable, or, wrapping none, the verdict its `code` gives (`timeout`, `storage_timeout` and `conflict` retryable, `server_error` retryable unless storage answered a `501`, `unreachable` and `redirected` the environment's). A pipe I/O answer whose `is_valid` is not a boolean, or whose `input_form` does not describe the pipe it selected, is a plain `InputPreparationError` with a `runtime` verdict, since the route broke its own contract.
|
|
236
240
|
|
package/docs/run-results.md
CHANGED
|
@@ -56,6 +56,26 @@ const results = await client.waitForResult(ack.pipeline_run_id);
|
|
|
56
56
|
|
|
57
57
|
Against a bare runner the id identifies the call the runner just answered, but there is no run store behind it: the lifecycle routes are absent, so re-reading it raises `RunLifecycleUnavailableError`. Durable resumption is a hosted capability.
|
|
58
58
|
|
|
59
|
+
**Holding the id while `startAndWaitForResult` waits.** `startAndWaitForResult` returns only the result, so a caller that waits through it would otherwise learn the run's id only once the run is over. Its second argument, `StartAndWaitForResultOptions`, takes the wait's options plus `onStarted`, called once with the start acknowledgement as soon as the durable run exists and before the first poll: the moment to show the id, log it, or keep it for an interrupt, since a caller that stops waiting (its `signal` aborted, a `RunTimeoutError`) leaves the run going on the server and resumes it with `waitForResult(id)`.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
process.once("SIGINT", () => controller.abort());
|
|
64
|
+
const results = await client.startAndWaitForResult(
|
|
65
|
+
{ method_ref: "github.com/acme/methods/receipt-review@v1.0.0", inputs },
|
|
66
|
+
{
|
|
67
|
+
signal: controller.signal,
|
|
68
|
+
onStarted: (ack) => console.error(`Run started: ${ack.pipeline_run_id}`),
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
```
|
|
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.
|
|
74
|
+
|
|
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
|
+
|
|
77
|
+
**Knowing whether a run may exist.** `onStarting`, the other callback `StartAndWaitForResultOptions` takes, is called right before each request that may create a run is sent: `POST /v1/start`, and the blocking `POST /v1/execute` of a bare runner or of the fallback to it, so it is called twice when a runner that looked hosted refuses the start. Until it is called no run exists, and once the `signal` has aborted it is not called and no such request is sent, so a caller that stops waiting before then can say that no run was started; from then until `onStarted`, a run may exist that the API has not yet named. It runs synchronously and its return value is ignored; an exception it throws propagates before the request is sent. The `pipelex-sdk` command words its interrupt message by it ([`cli.md`](cli.md)).
|
|
78
|
+
|
|
59
79
|
## `main_stuff` — the output
|
|
60
80
|
|
|
61
81
|
`main_stuff` is the resolved content of the run's main output and is always present for a completed run, unless the read's `artifacts` selection left it out. On the hosted path it is the `main_stuff.json` artifact; on the blocking path the SDK resolves it out of the returned working memory through the response's `main_stuff_name`. Both deliver the same content shape, so there is no shape-guessing and no path-dependent branch to write. A completed run that cannot deliver one throws `MissingMainStuffError` rather than handing back a half-filled result.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipelex/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.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": {
|
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
},
|
|
15
15
|
"type": "module",
|
|
16
16
|
"sideEffects": [
|
|
17
|
+
"./dist/cli.js",
|
|
17
18
|
"./dist/hooks/claude-mthds-check.js"
|
|
18
19
|
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"pipelex-sdk": "dist/cli.js"
|
|
22
|
+
},
|
|
19
23
|
"main": "./dist/index.js",
|
|
20
24
|
"types": "./dist/index.d.ts",
|
|
21
25
|
"exports": {
|