@pipelex/sdk 0.38.0 → 0.39.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/dist/runs.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { RequestArgumentError, RunFailedError, RunTimeoutError } from "./errors.js";
2
2
  import { MAX_TIMER_DELAY_MS } from "./timers.js";
3
+ import { dropUnfit, isPlainObject } from "./error-models.js";
3
4
  const TERMINAL_RUN_STATUSES = new Set([
4
5
  "COMPLETED",
5
6
  "FAILED",
@@ -15,6 +16,23 @@ export function isTerminalRunStatus(status) {
15
16
  export function isSuccessRunStatus(status) {
16
17
  return status === "COMPLETED";
17
18
  }
19
+ /**
20
+ * A status read's `degradation`, checked field by field so that it never fails the poll carrying
21
+ * it: a value that is not an object, or whose `reason` is not a string, reads as `null`, since a
22
+ * degradation without a reason cannot be branched on; `message`, `failing_since` and `ends_at` are
23
+ * kept when they are strings and `failed_attempts` when it is an integer, `null` kept for each;
24
+ * members this SDK does not name are relayed as sent.
25
+ *
26
+ * Internal: `getRunStatus` applies it.
27
+ */
28
+ export function readRunDegradation(value) {
29
+ if (!isPlainObject(value) || typeof value.reason !== "string")
30
+ return null;
31
+ const degradation = { ...value };
32
+ dropUnfit(degradation, ["message", "failing_since", "ends_at"], (field) => typeof field === "string");
33
+ dropUnfit(degradation, ["failed_attempts"], Number.isInteger);
34
+ return degradation;
35
+ }
18
36
  // ── Results artifact selection ───────────────────────────────────
19
37
  /**
20
38
  * The result artifacts a results read can be narrowed to, in the platform's order. Passing some
package/dist/runs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"runs.js","sourceRoot":"","sources":["../src/runs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runs.js","sourceRoot":"","sources":["../src/runs.ts"],"names":[],"mappings":"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"}
package/dist/version.d.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  * `User-Agent` without importing the public barrel. Kept in sync with
4
4
  * `package.json` — enforced by `tests/index.test.ts`.
5
5
  */
6
- export declare const SDK_VERSION = "0.38.0";
6
+ export declare const SDK_VERSION = "0.39.0";
package/dist/version.js CHANGED
@@ -3,5 +3,5 @@
3
3
  * `User-Agent` without importing the public barrel. Kept in sync with
4
4
  * `package.json` — enforced by `tests/index.test.ts`.
5
5
  */
6
- export const SDK_VERSION = "0.38.0";
6
+ export const SDK_VERSION = "0.39.0";
7
7
  //# sourceMappingURL=version.js.map
@@ -99,8 +99,9 @@ Flat `src/` (mirrors `mthds-js`'s `runners/api` flatness — the SDK has one cli
99
99
  - `runs.ts` — the run-lifecycle types (`RunStatus`, `RunRead`, `RunResults`, `RunResultState`) and the single poll loop (`pollUntilResult`). `RunResults.main_stuff` (the resolved main output content) is always present for a completed run: on the hosted path it is the `main_stuff.json` S3 artifact; on the bare-runner blocking path the SDK resolves it from the returned working memory via the response's `main_stuff_name`, so both paths deliver the same shape. Consumers read `main_stuff` directly. `working_memory` — every named stuff of the run, `{ root, aliases }`, typed `DictWorkingMemory` from `models.ts` — reads the same on both paths: the hosted path relays the `working_memory.json` artifact, and the blocking path lifts it off `pipe_output`, which still rides whole (blocking path only). `graph_spec` — the executed graph — also reaches the client on both paths: the hosted path relays the `graphspec.json` artifact, and the blocking path lifts it off `pipe_output` beside `graph_assembly_error`, the graph's twin of `usage_assembly_error`. That error field is declared ahead of the wire on the hosted side, where the platform's results body relays no such key and the field is therefore absent, reading `undefined` rather than `null`. A completed run that cannot deliver a main stuff throws `MissingMainStuffError` when the read asked for it. `runs.ts` also declares `RUN_RESULT_ARTIFACTS` / `RunResultArtifact`, the artifacts a results read can be narrowed to with `getRunResult`'s `artifacts` option; with a selection an unselected artifact is absent while a selected unwritten one is `null`. `runs.ts` also owns `TokensUsageRecord` — one client-facing usage record per inference call, carried by `RunResults.tokens_usages` on both paths. That is a mirror of a wire contract this SDK does not own (inference accounting is a Pipelex runtime extension rather than part of the MTHDS standard): every field is optional and the interface carries an index signature so pre-contract artifacts (relayed verbatim, never migrated) still type-check. See [`run-usage.md`](./run-usage.md) for the field reference, the cost/null semantics, and the old-artifact rules, and [`run-results.md`](./run-results.md) for the field-by-field account of `RunResults` — the durable handle, the output and how to read it, the run's working memory, the executed graph and how to render or keep it, and the produced files with their short-lived links.
100
100
  - `usage.ts` — `summarizeUsage`, the **pure** fold of a run's usage pair (`tokens_usages` / `usage_assembly_error`) into one `UsageSummary`: a `state` (`records`, `no_inference` or `unavailable`), the null-aware total cost with its `cost_partial` flag, the `input` and `output` token totals, the call count, the relayed assembly error, and a `by_pipe` rollup sorted by cost with the unattributed calls grouped under a `null` `pipe_code`. It applies the rules [`run-usage.md`](./run-usage.md#summarizing-a-run--summarizeusage) states, so that every consumer reads one computation rather than re-deriving those rules by hand. It imports types from `runs.ts` and nothing else: no client, no fetch, no `mthds/protocol`.
101
101
  - `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.
102
- - `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).
102
+ - `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. The base also carries `runMayHaveStarted`, false unless a request that may create a run failed in a way that may have started one, and `runMayHaveStartedOf` reads it structurally the same way. 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).
103
103
  - `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 drives its own code through the identical copy `make shared-files` writes.
104
+ - `run-may-have-started.ts` — the **pure** reading of whether a failed request that may create a run may still have started one: `answerMayHaveStartedRun(status, code)` for an answer and `transportMayHaveStartedRun(cause)` for a transport failure, every failure counting unless it proves otherwise (a failure of the connection's set-up, a `501`, the platform's `503` coded `pipeline_start_unavailable`, or any status that refuses the request). The client marks the requests that may create a run, `start` and the blocking `execute`, with a `RunCreation` flag it threads through `requestRaw`, `readObjectAnswer` and its error helpers into the constructors of `ApiUnreachableError` and `ApiResponseError`, which apply the reading and carry the answer as `runMayHaveStarted`, forcing `retryable` false when it is true; nothing changes an error after it is thrown. The Python twin is `pipelex_sdk/run_may_have_started.py`. See [errors.md](./errors.md#a-failed-run-start--runmayhavestarted).
104
105
  - `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).
105
106
  - `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.
106
107
  - `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.
@@ -117,7 +118,7 @@ Flat `src/` (mirrors `mthds-js`'s `runners/api` flatness — the SDK has one cli
117
118
  - **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).
118
119
  - **Model reference check (`/v1/models/check`):** `checkModelReference(reference, category?)`, a Pipelex API extension served by any `pipelex-api` runner from pipelex 0.78.0 and on the hosted API, whose platform relays it byte-identically. It is a **diagnostic endpoint** like `validate`: a reference that resolves nowhere is a `200` verdict with `resolution: "not_found"` and the names the caller may have meant (`suggestions`, `other_kinds`, `other_categories`), never a thrown error. Only a request that cannot produce a verdict throws `ApiResponseError`, a `422` typed by its `errorType`: `InvalidModelReference` for a reference that is blank, a sigil or a namespace alone, or past the runner's length limit, `InvalidModelCategory` for an unknown `type`, `ValidationError` for a missing or repeated parameter. The SDK validates none of it, so the runner's rule is the only one. `ModelReferenceVerdict` is a union of one arm per reference kind, discriminated on the wire's own `kind` (`PresetReferenceVerdict`, `AliasReferenceVerdict`, `WaterfallReferenceVerdict`, `HandleReferenceVerdict`): a `matches` entry carries no kind of its own, but every entry of one verdict shares the verdict's, so narrowing on `kind` types `matches` as `PresetMatch[]`, `AliasMatch[]`, `WaterfallMatch[]` or `HandleMatch[]` without a field the wire does not carry. `ModelCheckCategory` is the protocol's model categories plus `doc_gen`, the family of `PipeDocGen`, which the protocol and so `models` leave out but a method's `model` field can name. `resolution` is typed open, `ModelReferenceResolution | (string & {})`, under the spec's reader rule that a client reading a value it does not know treats the reference as unresolved, and so is every category a verdict names, `ModelCheckCategory | (string & {})` as `mthds` types `ModelInfo.type`, since a runner of a later version may add one; the `category` a request names stays closed. A match's `resolves_to` is `null` when the reference resolves by name but a run through it would reach no model the runner can call, which a consumer shows as a warning rather than as an unknown name.
119
120
  - **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.
120
- - **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).
121
+ - **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). The status read also carries `degraded` and the `degradation` that explains it, whose `reason` tells a status the platform could not refresh (`status_unavailable`) from a run whose workflow task keeps failing until the platform ends it (`workflow_task_failing`), checked field by field so it never fails the poll, and the run's `request_id`, the id to quote to support. See [run-results.md](./run-results.md#a-degraded-status--degraded-and-degradation). 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).
121
122
  - **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.
122
123
 
123
124
  ### Product routes (hosted management surface)
package/docs/cli.md CHANGED
@@ -79,7 +79,7 @@ Every error goes to stderr, its first line `Error: <sentence>` and the lines und
79
79
  - **An unreachable API**: `Error: could not reach the Pipelex API at <base URL> (<code>).`
80
80
  - **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.
81
81
  - **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`.
82
- - **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
+ - **A start whose outcome is unknown**: once the command has reached a request that may create a run, the start or a bare runner's blocking execute, and before the API names the run, a failure the SDK reads as one that may have started a run, its error's `runMayHaveStarted` ([`errors.md`](errors.md#a-failed-run-start--runmayhavestarted)), 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 the command cannot read, a `408`, which a proxy in front of the platform may answer for a request it forwarded, any `5xx` but a `501` and the platform's `503` with the code `pipeline_start_unavailable`, which says the runner was never reached, a request the gateway cut off, and a transport failure that does not prove the request never left, such as an answer lost to a time limit or to a connection that closed once the request had left. Timing plays no part: a `503` or `504` that came back at once reads the same as one at the gateway's limit, since the platform can answer a `503` after the runner has started the run. Any other answer from the API, 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, each through its SDK's member, 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`.
83
83
 
84
84
  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.
85
85
 
package/docs/errors.md CHANGED
@@ -30,26 +30,28 @@ try {
30
30
 
31
31
  `errorVerdictOf` checks the shape rather than the class: it returns the pair from any `Error` carrying a boolean `retryable` and a known `errorDomain`, and `undefined` otherwise. So it reads an error raised by another copy of the SDK installed beside yours, a subclass of your own, and an `mthds` `ApiResponseError` whose runner sent both members. Every class also exposes the two members directly, and they derive from the abstract `PipelexRequestError`, itself a `PipelineRequestError`, so `instanceof PipelineRequestError` still matches all of them.
32
32
 
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.
33
+ **"Retryable" means a retry can succeed, and never one that could start a second paid run.** The verdict says whether asking again can plausibly succeed, and on most routes it says nothing about whether the first attempt had an effect. A request that may create a run, a `start` or a blocking `execute`, is the exception: when it failed in a way that may have started a run anyway, the error's `runMayHaveStarted` is `true` and its `retryable` is `false`, whatever the server sent and whatever the status says, since asking again could start the run twice. [A failed run start](#a-failed-run-start--runmayhavestarted) says when that is and what to tell a person.
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.
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 blocking `execute`, or a `start` whose server is fetching a `method_ref`'s package. `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 and for any other failure. The blocking `execute` applies it and throws `PipelineExecuteTimeoutError`, timing the request on the monotonic clock `performance.now`, since a change of the system's time moves `Date.now`. A caller no longer needs it to learn whether a failed start may have created a run: the error's `runMayHaveStarted` says so whatever the timing. 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
36
 
37
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 artifact selection that is not an array), 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.
38
38
 
39
39
  ### Each class's verdict
40
40
 
41
+ On an error from a request that may have started a run, `retryable` is `false` whatever this table says ([below](#a-failed-run-start--runmayhavestarted)).
42
+
41
43
  | Class | `errorDomain` | `retryable` | Why |
42
44
  |---|---|---|---|
43
45
  | `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
46
  | `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, a selector `parseMethodSelector` refuses, or an artifact selection that is empty or names an unknown artifact. 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
47
  | `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
- | `PipelineExecuteTimeoutError` | `input` | no | The blocking `execute` cannot outlast the gateway's limit; start the run and poll instead. |
48
+ | `PipelineExecuteTimeoutError` | `input` | no | The blocking `execute` cannot outlast the gateway's limit; start the run and poll instead. The run goes on, so its `runMayHaveStarted` is always `true`. |
47
49
  | `RunFailedError` | the report's | the report's | See [a failed run](#a-failed-run--runerrorreport). |
48
50
  | `RunTimeoutError`, `RunStillRunningError` | `runtime` | yes | The run is still going; waiting again, by its id, can succeed. |
49
51
  | `RunLifecycleUnavailableError` | `config` | no | The base URL points at a bare runner without the run lifecycle. |
50
52
  | `MissingMainStuffError` | `runtime` | no | The API broke its own contract on a completed run. |
51
53
  | `PagingNotTerminatingError` | `runtime` | no | A server minting cursors forever; `pageLimit` is the ceiling `iterateMethods` stopped at. |
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. |
54
+ | `InputPreparationError` and `EmptyMethodSourceError`, `InvalidLocalSourceError`, `InvalidInputValueError`, `UploadNotAllowedError`, `MethodLoadError`, `RejectedAssetError` | `input` | no | The asset, the inputs or the method selector must change. The family's base takes an optional `verdict` in its options, so a subclass of your own can declare another. |
53
55
  | `UnsupportedUploadCapabilityError`, `UploadAuthenticationError` | `config` | no | The deployment or the credential must change. |
54
56
  | `UploadTransportError` | the cause's, else by `code` | the cause's, else by `code` | `uploadFile` wraps the `ApiResponseError` or `ApiUnreachableError` the client threw, and takes its verdict, so a wrapped `402` is `config` and not retryable. Without such a cause, which is always the case from `uploadWithGrant`: `timeout`, `storage_timeout` and `conflict` are `runtime` and retryable, `conflict` because storage documents its `409 ConditionalRequestConflict` as retryable and the grant is not spent by it (a spent grant is the `412` of a `RejectedAssetError` with code `grant_used`); `server_error` is `runtime`, and retryable when [the fallback table](#a-refused-requests-verdict) would call its `status` retryable, any `5xx` but the `501` storage answers for a request it does not implement, or when it carries no status; `unexpected` is `runtime`, and retryable when that table would call its `status` retryable, which is the `408` or `429` storage answers when it times out or throttles the request rather than refusing the file, and not retryable for any other status or none; `unreachable` is `config` and retryable; `redirected` is `config` and not retryable; `invalid_grant_url` is `runtime` and not retryable. |
55
57
  | `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,9 +81,54 @@ The fallback reads the HTTP status, the platform's `code`, and whether the body
79
81
 
80
82
  The members the server sent are read first, and the fallback answers only for one it did not send: a runner's problem usually carries its own, the platform sends them on the problems it renders itself once its deployment classifies them, and a route that sends neither, as the platform's Lambda-backed routes do not yet, gets the fallback for both. **Nothing on the error says which of the two a value came from**: `problemDocument` keeps the document whole, so a reader that must tell a server-sent verdict from a derived one reads it there, as the live suite `tests/e2e/error-verdicts.e2e.ts` does. A rejected API key always gets the fallback's verdict: the hosted gateway's authorizer refuses it before the platform sees the request, with a bare `403` whose body is `{"message":"Forbidden"}`, which carries no `code`, no `type` and no verdict, so the error reads it as `config` and not retryable from the status alone.
81
83
 
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 file is the source: `make shared-files` at the repository's root writes its byte for byte identical copy to `python/tests/fixtures/error-verdicts.json`, `make check-shared-files` fails on a stale copy, and the Python suite drives its own code through the same cases (`python/docs/errors.md`). A class or a variant only one SDK has is marked `only_in` in the file, and the other suite skips it. So a change to a verdict is made in the file here, copied, and then made in both packages.
84
+ **The verdicts are a contract with the Python SDK.** `pipelex-sdk` must apply the same fallback, declare the same verdict on each class it shares with this package, and read a failed run start the same way. `tests/fixtures/error-verdicts.json` holds every case the two must agree on, the fallback's rows, the server-sent resolution, each class's variants and the answers to a run-creating request (its `run_start` member, each row sent through a real `start` or `execute`), and this package's test suite drives its own code through it. The file is the source: `make shared-files` at the repository's root writes its byte for byte identical copy to `python/tests/fixtures/error-verdicts.json`, `make check-shared-files` fails on a stale copy, and the Python suite drives its own code through the same cases (`python/docs/errors.md`). A class or a variant only one SDK has is marked `only_in` in the file, and the other suite skips it. So a change to a verdict is made in the file here, copied, and then made in both packages.
85
+
86
+ The shapes are not this SDK's invention. The report is the runner's `ErrorReport` (in `pipelex`), stored by the platform exactly as the runner wrote it and served through the runner's STRICT disclosure, as [below](#what-the-platform-serves-of-a-report), 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.
87
+
88
+ ### A failed run start — `runMayHaveStarted`
89
+
90
+ Every error the SDK throws carries a third member beside the verdict, `runMayHaveStarted`, a boolean that is never `undefined`. It is `true` when the request that failed may have started a run whose id never reached the caller, and `false` otherwise, which includes every error from a request that cannot start a run. Read it with `runMayHaveStartedOf`, which takes anything a `catch` holds and, like `errorVerdictOf`, checks the shape rather than the class: it is `true` only for an `Error` whose `runMayHaveStarted` is `true`, so it reads an error raised by another copy of the SDK, and an `mthds` error, which never carries the member, reads as `false`.
91
+
92
+ **The requests that may create a run** are `start` and the blocking `execute`, and inside `startAndWaitForResult` the start and the blocking execute it falls back to against a bare runner. Once `onStarted` has been called the run exists and the caller holds its id, so an error while the composite waits started nothing: its `runMayHaveStarted` is `false`, and its `retryable` is about waiting again on that id, never about calling the composite again. `RunStillRunningError`, the `202` a blocking execute answers for a run still going, names its run, so its member is `false` too; a `202` that names no run is an answer the SDK could not read, which may have started one.
93
+
94
+ A failure of a request that may create a run may have started one in these cases, and only these:
95
+
96
+ | Failure | May have started | Why |
97
+ |---|---|---|
98
+ | `ApiUnreachableError` that proves nothing was sent | no | The connection's set-up failed, so no request followed. |
99
+ | Any other `ApiUnreachableError`, a body that could not be decoded included | yes | The request may have left, and the answer was lost or unreadable. |
100
+ | `ApiResponseError` with a `2xx` status | yes | The server accepted the request; only its answer could not be read, which includes a `start` acknowledgement or an `execute` `202` without a string `pipeline_run_id`, and an `execute` answer that is not the completed run. |
101
+ | `ApiResponseError` with a `408` | yes | A proxy in front of the platform may answer it for a request it forwarded. |
102
+ | `ApiResponseError` with a `503` and the code `pipeline_start_unavailable` | no | The platform never reached the runner, and recorded the run as `FAILED`. |
103
+ | `ApiResponseError` with a `501` | no | The deployment does not serve the route. |
104
+ | `ApiResponseError` with any other `5xx` | yes | The server or a gateway failed while handling the request. |
105
+ | `ApiResponseError` with any other status | no | The server refused the request before running anything. |
106
+ | `PipelineExecuteTimeoutError` | yes | The gateway cut the execute off, and the run goes on. |
107
+ | `RunLifecycleUnavailableError`, `RequestArgumentError`, an input preparation error | no | Nothing that starts a run was sent. |
108
+
109
+ **The reading errs on the safe side.** A false "may have started" costs a look at the run history; a false "did not start" costs a second paid run. So every `5xx` counts unless the server says otherwise, as the `501` and the coded `503` do. A `503` without that code can follow a run the runner already started, since the platform answers a failure of its own storage with one after it has dispatched the start. Timing plays no part: a `503` or `504` that came back at once reads the same as one at the gateway's limit.
83
110
 
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.
111
+ **What proves nothing was sent** is a failure of the connection's set-up, which no request follows: the name lookup or the connect call failing (the system calls `getaddrinfo` and `connect`: an unknown host, a refused connection, no route to the host or the network, the system's connect time limit), undici's own connect time limit, which covers the TLS handshake (`UND_ERR_CONNECT_TIMEOUT`), and a server certificate the TLS handshake refused. Several addresses each failing so, which Node reports together as an `AggregateError`, prove it too. Anything else may have come once the request had left: a reset (`ECONNRESET`), which Node reports alike whether the connection dropped during the TLS handshake or after the request; another TLS failure; the SDK's own time limit (`ABORT_TIMEOUT`), which fetch cannot place before or after the connection was made; and a body that arrived and could not be decoded.
112
+
113
+ **A run that may have started is never retryable.** When `runMayHaveStarted` is `true`, `retryable` is `false`, whatever the problem document sent and whatever the fallback table says; `errorDomain` is unchanged, and the server's own members stay on `problemDocument`. So a caller that branches on `retryable` alone never offers a retry that could start a second paid run. A caller that reads the member can say more: tell the person the run may exist, and to look it up in the run history (`listRuns`) before starting it again.
114
+
115
+ ```ts
116
+ import { errorVerdictOf, runMayHaveStartedOf } from "@pipelex/sdk";
117
+
118
+ try {
119
+ await client.start({ method_id: "mt_abc123", inputs });
120
+ } catch (err) {
121
+ if (runMayHaveStartedOf(err)) {
122
+ return tell("The run may have started. Check the run history before starting it again.");
123
+ }
124
+ if (errorVerdictOf(err)?.retryable) return scheduleRetry();
125
+ throw err;
126
+ }
127
+ ```
128
+
129
+ **What lifts the rule** is a start that is safe to retry. Once the platform replays a start sent again under the same `Idempotency-Key`, the SDK can send one on every start and report such a failure as retryable under the same key. The SDK sends no key today.
130
+
131
+ The Python SDK carries the same member as `run_may_have_started`, with the reader `run_may_have_started_of`, and reads every failure the same way; the `run_start` rows of the shared case file hold the two to the same answer for every status.
85
132
 
86
133
  ## A failed run — `RunErrorReport`
87
134
 
@@ -127,27 +174,48 @@ The report's fields, all optional because the runner owns the shape:
127
174
 
128
175
  | Field | Meaning |
129
176
  |---|---|
130
- | `message` | What went wrong, as the runner wrote it. For a run failure it is where the failing pipe is named. |
177
+ | `message` | What went wrong. For a run failure it is where the failing pipe is named. A hosted read serves the author's copy only when `caller_facing_message` is `true`, and the placeholder `An internal error occurred.` otherwise. |
131
178
  | `error_domain` | Who can fix it: `input` (the caller — a malformed method, a bad input), `config` (a configuration change — a model the backend does not serve, a missing secret), `runtime` (nobody beforehand — a provider outage). **A branch field.** |
132
179
  | `type_uri` | The stable URI naming the error class, the same on every occurrence. **A branch field.** |
133
180
  | `retryable` | Whether running it again can succeed. Absent means unknown, which is not `false`. **A branch field.** |
134
- | `user_action` | The next step, as `{ kind, detail }`: `detail` is the advice in words, `kind` its category (`wait_and_retry`, `check_billing`, `check_credentials`, `change_input`, `change_model`, `contact_support`, `unknown`). |
181
+ | `user_action` | The next step, as `{ kind, detail }`: `detail` is the advice in words, `kind` its category (`wait_and_retry`, `check_billing`, `check_credentials`, `change_input`, `change_model`, `contact_support`, `unknown`). A hosted read serves it only beside caller-facing copy. |
135
182
  | `title` | The stable human label of the error class. |
136
183
  | `error_type` | The runner's exception class name — an open set, for display and support, never matched against. |
137
184
  | `error_category` | The finer class of an inference failure: `transient`, `configuration`, `content`, `capacity`, `ambiguous`, `unknown`. |
138
- | `model`, `provider` | The model and provider an inference failure involved. |
139
- | `provider_metadata` | What the provider's SDK said: `provider`, `sdk_exception_type`, `message`, `status_code`, `request_id` (the provider's own), `retry_after_seconds`, `provider_error_code`. |
185
+ | `model`, `provider` | The model and provider an inference failure involved. Absent on a hosted read. |
186
+ | `provider_metadata` | What the provider's SDK said: `provider`, `sdk_exception_type`, `message`, `status_code`, `request_id` (the provider's own), `retry_after_seconds`, `provider_error_code`. A hosted read keeps only `status_code` and `retry_after_seconds`. |
140
187
  | `validation_errors` | The structured diagnostics of a method that failed validation, the same [items](#a-validation-item--validationerroritem) the validate report carries. |
141
- | `migration` | A pending configuration migration that explains the failure, present only when the runner's scan found one. |
142
- | `caller_facing_message` | `true` when `message` was written as caller-facing copy. |
188
+ | `migration` | A pending configuration migration that explains the failure, present only when the runner's scan found one. Absent on a hosted read. |
189
+ | `caller_facing_message` | `true` when `message` was written as caller-facing copy. A hosted read keeps it on a caller-facing report and leaves it out otherwise. |
143
190
 
144
191
  A field a newer runner adds is reachable through the interface's index signature before this SDK names it.
145
192
 
146
193
  **The report is checked field by field, never cast.** Every read that hands one back applies the same check: a value that is not an object reads as no report (`null`); each named field is kept when it has the type the table above gives it and is absent otherwise, so a `message` that is a number or a `retryable` of `"no"` never reaches you as typed; `null` is kept wherever the type allows it, and an empty string is kept, since deciding that a blank field says nothing is presentation. `provider_metadata` and `migration` are checked one level down, `validation_errors` keeps only the items that are objects with a string `category` and `message`, and members the SDK does not name, in the report or in a nested object, are relayed as sent. **`user_action` is kept whole or not at all**: an object whose `kind` and `detail` are both strings, which is what its type promises. This is the one place the check differs from the Python SDK's, which keeps a partial action, its stored-report action being a lenient type of its own. On the run records, the check runs only when the record carries the `error` key, so a server that does not serve it still leaves it absent.
147
194
 
148
- **The report is `null` when the run has none.** A run the platform finalized itself carries no report: one whose start failed, and a timeout, a termination or a vanished workflow resolved by the platform. A cancelled run usually has none either. The status and the `detail` sentence still say what happened, and the absence of a report says nothing about why.
195
+ **A run the platform ends or settles itself carries the platform's report.** Some endings have no runner to write a report, and the platform writes one instead, in the runner's shape, so it reads like any other:
196
+
197
+ | `error_type` | When | `error_domain` | `retryable` | `user_action.kind` |
198
+ |---|---|---|---|---|
199
+ | `WorkflowTaskKeptFailingError` | The run's workflow task kept failing for the whole failure window, and the platform ended the run `FAILED`. While the window runs, the status read says so as a `workflow_task_failing` degradation ([run-results.md](./run-results.md#a-degraded-status--degraded-and-degradation)). | `runtime` | `false` | `contact_support` |
200
+ | `RunTimedOutError` | The workflow engine timed the run out, and it ended `TIMED_OUT`. | `runtime` | `false` | `contact_support` |
201
+ | `RunWorkflowNotFoundError` | The run's workflow cannot be found: it aged out of the engine's retention, or the run was lost while starting. The run ends `FAILED`. | `runtime` | `true` | `wait_and_retry` |
202
+
203
+ These reports are caller-facing copy: they quote nothing of the worker's own failure text, their `caller_facing_message` is `true`, and so they keep their message and their next step on a hosted read. The one exception is a `WorkflowTaskKeptFailingError` report rebuilt from a termination an earlier platform recorded, whose message quotes the worker's failure: it is not caller-facing, so it reads as the placeholder.
204
+
205
+ **A cancelled, terminated or timed-out run holds a report when one was sent.** Its `error` holds the report a completion callback carried, or, on a `TIMED_OUT` run, the `RunTimedOutError` report the platform writes for the timeout. A run the platform ended because its workflow kept failing is `FAILED`, never `TERMINATED`.
206
+
207
+ **The report is `null` when the run has none**: a completed run, one whose start failed, a blocking `execute` run, whose answer carried its reason, one cancelled, terminated or given its status by hand with no completion callback carrying a report, and one that ended before the platform wrote reports of its own. The status and the `detail` sentence still say what happened, and the absence of a report says nothing about why.
208
+
209
+ ### What the platform serves of a report
210
+
211
+ **The platform serves the runner's STRICT projection, and the SDK strips nothing further.** The run row keeps the runner's report whole, as the record of the people who operate the platform, but every read that hands it to a caller (the status read, the run records and the results read's `409`) serves it through the rule the hosted runner applies to an error on `execute`:
212
+
213
+ - `error_type`, `title`, `type_uri`, `error_domain`, `error_category`, `retryable` and `validation_errors` pass through when present;
214
+ - `message` and `user_action` pass through only when the report's `caller_facing_message` is `true`, which the served report then keeps; otherwise `message` is the placeholder `An internal error occurred.`, and `user_action` and `caller_facing_message` are absent;
215
+ - `provider_metadata` keeps only `status_code` and `retry_after_seconds`, and is absent when neither is present;
216
+ - `model`, `provider` and `migration` are absent.
149
217
 
150
- **Nothing is stripped, so presentation is yours.** The platform serves the runner's VERBOSE report, so `message` and `provider_metadata.message` can hold a provider's raw text. The SDK types what the platform sends; deciding what a person sees is the consumer's decision. `RunFailedError.message`, being the `detail`, already contains the report's message.
218
+ So `caller_facing_message` is how a consumer tells the author's copy from the placeholder, and `message` can be shown as it comes: it never carries the worker's internal text or a provider's raw answer. `RunFailedError.message`, being the `detail`, already contains the served message. Every deployed environment serves the projection; a platform run locally with `ERROR_DISCLOSURE=verbose` serves the report whole, which is why the type still names every member the runner writes. Deciding what a person sees is still the consumer's decision.
151
219
 
152
220
  **Numbers in `provider_metadata` may arrive as strings.** A report read back from the platform's run store carries `status_code` and `retry_after_seconds` as strings (`"404"`, `"1.5"`), because the store returns its numbers as decimals that the platform serializes as strings; a problem document a runner renders carries them as numbers. Both are in the type, so read them with `Number(...)`.
153
221
 
@@ -155,13 +223,14 @@ A field a newer runner adds is reachable through the interface's index signature
155
223
 
156
224
  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
225
 
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.
226
+ **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. On `start` and `execute` its `runMayHaveStarted` is `true`, since the server accepted the request; that includes a start acknowledgement whose `pipeline_run_id` is not a string, and an `execute` `202` that names no run, which leave the caller a run it could not follow, and an `execute` answer that is not the completed run: one whose `pipeline_run_id` is not a string, or whose `pipe_output` carries no string `pipeline_run_id` or no `working_memory` with `root` and `aliases` objects, the members the Python SDK's model of the answer requires. A product route that answers a `2xx` with no body at all, such as `revokePipelexApiKey`, still resolves to `undefined`, and the other product routes hand their decoded body back as it came.
159
227
 
160
228
  | Field | Wire member | Meaning |
161
229
  |---|---|---|
162
230
  | `errorDomain` | `error_domain`, else the fallback | Who can fix it: `input`, `config` or `runtime`. Always decided. **A branch field.** |
163
231
  | `type` | `type` | The stable URI naming the error class. **A branch field.** |
164
- | `retryable` | `retryable`, else `false` beside a sent `input` or `config` domain, else the fallback | Whether retrying the same request can succeed. Always decided. |
232
+ | `retryable` | `retryable`, else `false` beside a sent `input` or `config` domain, else the fallback | Whether retrying the same request can succeed. Always decided, and `false` whenever `runMayHaveStarted` is `true`. |
233
+ | `runMayHaveStarted` | none | Whether a `start` or a blocking `execute` that failed may have started a run anyway ([above](#a-failed-run-start--runmayhavestarted)). Always decided. |
165
234
  | `userAction` | `user_action` | The next step, `{ kind, detail }`. |
166
235
  | `requestId` | `request_id`, else the `X-Request-ID` header | The id to hand to support: it finds the server's log lines for the request. |
167
236
  | `title` | `title` | The stable human label of the error class. |
@@ -143,6 +143,21 @@ The route selects the pipe, and the ref it answers with is the one prepared —
143
143
 
144
144
  There is no single-pipe fallback: a method that declares one pipe and no `main_pipe` is refused a selector-less run, and a selector-less preparation with it.
145
145
 
146
+ #### Preparing without uploads
147
+
148
+ ```
149
+ client.prepareInputs({ method_id, inputs, allowUploads: false }) → PreparedInputs
150
+ ```
151
+
152
+ `allowUploads: false` prepares the inputs without uploading anything, for a server that prepares inputs on a caller's behalf and holds no files of its own, such as a hosted MCP connector. Such a server must never read a file from its own disk because a caller named a path, and in the default mode the read happens inside the walk, where no seam the server controls could refuse it first. Under this option:
153
+
154
+ - a value at a file position that would need an upload — a local path, a `data:` URL, bytes (`Blob`, `File`, `ArrayBuffer`, `Uint8Array`), whether bare, inside canonical `{ url }` content or inside an explicit envelope — throws `UploadNotAllowedError` **before** anything is read from disk, decoded or sent to the upload route. A `data:` URL is refused without being decoded, so a malformed one is refused the same way;
155
+ - `http(s)` URLs and `pipelex-storage://` references pass through and are rewritten exactly as in the default mode, and `uploads` is always empty;
156
+ - a value no file input takes (a number, an object that is neither bytes nor `{ url }` content) is still an `InvalidInputValueError`, as in the default mode, since it would need no upload, only a correction;
157
+ - everything that is not a file position is untouched, as always: the walk is the same descriptor walk.
158
+
159
+ The error names the input as `inputName` and where inside it the value sits as `inputPath`: the input's name for a top-level file, then `.field` for each declared field and `[index]` for each list element (`dossier.attachments[1]`). An explicit envelope adds nothing to the path, since its `content` is the input's value. `valueKind` says what the value is, `local_path`, `data_url` or `bytes`, so a consumer can tell its caller what to send instead; the value itself is never in the message or on the error, since it is the caller's data. The option is `true` when omitted, must be a boolean when given — anything else is refused with an `InputPreparationError` before any request, since reading a stray `"false"` as permission would upload exactly what the caller meant to keep — and is not sent to the server. The option covers `prepareInputs` alone: `uploadFile` is an upload by definition.
160
+
146
161
  ## `getMethodClosure` — the explicit expansion utility
147
162
 
148
163
  ```
@@ -214,6 +229,8 @@ A repeated reference to the **same source** within one preparation is uploaded o
214
229
  | Existing `pipelex-storage://` URI | Already prepared — pass through unchanged |
215
230
  | HTTP(S) URL | Pass through unchanged, **unless** the caller explicitly asks to ingest it into Pipelex storage |
216
231
 
232
+ Under `allowUploads: false`, the first row is refused with `UploadNotAllowedError` instead (see "[Preparing without uploads](#preparing-without-uploads)"), and the other two are unchanged.
233
+
217
234
  ## Rewritten-input shape: `url` carries the URI
218
235
 
219
236
  The runtime's canonical image/document content stores its reference in a **`url`** field. Preparation emits inputs the runtime interprets natively, so a rewritten input keeps the canonical content shape with `url` holding the `pipelex-storage://` value — exactly what the runtime's `input_normalizer` writes.
@@ -231,6 +248,7 @@ The contract distinguishes these semantic outcomes, each a typed subclass of `In
231
248
  - **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;
232
249
  - **invalid local source** (`InvalidLocalSourceError`) — missing, unreadable, or a path string outside Node;
233
250
  - **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;
251
+ - **upload not allowed** (`UploadNotAllowedError`, carries `inputName`, `inputPath` and `valueKind`) — under `allowUploads: false`, a value at a file input that would need an upload: a local path, a `data:` URL or bytes. It is raised before anything is read, decoded or uploaded (see "[Preparing without uploads](#preparing-without-uploads)");
234
252
  - **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`);
235
253
  - **unsupported server capability** (`UnsupportedUploadCapabilityError`) — the configured deployment has no upload route;
236
254
  - **authentication / authorization failure** (`UploadAuthenticationError`) — `401` / `403`;
@@ -76,6 +76,45 @@ It is never called on the blocking path, a bare runner's `POST /v1/execute` or t
76
76
 
77
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
78
 
79
+ **A failed start says whether a run may exist.** An error from the start, or from the blocking execute, carries `runMayHaveStarted`: `true` when the request may have created a run although the call failed, so a person looks the run up in the run history before starting it again, and then its `retryable` is `false` ([`errors.md`](errors.md#a-failed-run-start--runmayhavestarted)). Once `onStarted` has been called the run exists and the caller holds its id, so an error while the composite waits started nothing: its `runMayHaveStarted` is `false`, and its `retryable` is about waiting again on that id with `waitForResult`, never about calling `startAndWaitForResult` again, which would start a second run.
80
+
81
+ ## A degraded status — `degraded` and `degradation`
82
+
83
+ `getRunStatus(runId)` reads a run's status row, which is how a caller shows a run while it runs. It returns a `RunRead` (`src/runs.ts`): the run record plus `degraded`, `degradation`, `request_id` and `retry_after_seconds`, the last parsed from the `Retry-After` header.
84
+
85
+ **`degraded` means the read is not a plain fresh reading, and `degradation.reason` says why.** So `degraded` alone no longer means the run is fine; branch on the reason.
86
+
87
+ | `reason` | What holds | `status` | What to tell the person |
88
+ |---|---|---|---|
89
+ | `status_unavailable` | The platform could not reach the workflow engine. | The last one recorded. | Nothing about the run, which may be fine: the status is not fresh, and the next read comes after `retry_after_seconds`. |
90
+ | `workflow_task_failing` | The workflow task that drives the run has failed `failed_attempts` times in a row since `failing_since`, so the run is not making progress. | Live. | The run is stuck. The workflow engine keeps retrying, and if the task is still failing at `ends_at` the platform ends the run `FAILED`, with a `WorkflowTaskKeptFailingError` report ([errors.md](./errors.md#a-failed-run--runerrorreport)). |
91
+
92
+ ```ts
93
+ const run = await client.getRunStatus(runId);
94
+ if (run.degraded) {
95
+ // A platform older than the degradation sends none; `degraded` then meant only this.
96
+ const reason = run.degradation?.reason ?? "status_unavailable";
97
+ if (reason === "workflow_task_failing") {
98
+ const ends = run.degradation?.ends_at ?? "its time limit";
99
+ console.warn(`The run is stuck (${run.degradation?.failed_attempts} failures in a row); it ends at ${ends}.`);
100
+ } else if (reason === "status_unavailable") {
101
+ console.warn(`The status is not fresh; reading again in ${run.retry_after_seconds ?? 5}s.`);
102
+ } else {
103
+ console.warn(`The status read is degraded (${reason}).`); // a reason this SDK does not know yet
104
+ }
105
+ }
106
+ ```
107
+
108
+ - **`failing_since` and `ends_at` are `null` when the platform could not find the recorded failure** in the workflow's history. It then does not end the run itself, and the run's own time limit ends it `TIMED_OUT`, with a `RunTimedOutError` report. Both are always `null` for `status_unavailable`, and so is `failed_attempts`.
109
+ - **`message` is the platform's sentence.** For `workflow_task_failing` it says how many attempts have failed in a row, that the run is not making progress and when the platform will end it. It does not quote the worker's own failure text, which only a platform run locally with `ERROR_DISCLOSURE=verbose` adds, so a product surface may show it; `failed_attempts` and `ends_at` carry the same facts as fields. Never branch on its wording.
110
+ - **`reason` is read open**, typed `DegradationReason | (string & {})`: a reason a later platform adds arrives as its raw string rather than failing the read, and a consumer that does not know it should not tell the person the run is fine.
111
+ - **The degradation is checked field by field, so it never fails the poll.** A `degradation` that is not an object, or whose `reason` is not a string, reads as `null`, since it cannot be branched on; `message`, `failing_since` and `ends_at` are kept when they are strings and `failed_attempts` when it is an integer, `null` kept for each, and absent otherwise; members this SDK does not name are relayed as sent. `degradation` is `null` whenever `degraded` is false, and absent on a platform that predates it. `degraded` is left as the server sent it.
112
+ - **`request_id` is the id of the request that started the run**, the one to quote to support beside the run id. `getRunDetail` carries it too; it is `null` for a run saved before the platform recorded it.
113
+
114
+ **The wait loops never see a degradation, and need not.** `waitForResult` and `startAndWaitForResult` poll the results route, whose in-flight answer is a `202` with no body. That route settles the run the same way the status read does, so a waiter receives a stuck run's end as a `RunFailedError` carrying the `WorkflowTaskKeptFailingError` report within one poll of `ends_at`. A caller that wants to show the degradation while it waits reads `getRunStatus` beside the wait.
115
+
116
+ Both SDKs read a status body the same way, held to it by a case file they share, `tests/fixtures/run-status-reads.json`.
117
+
79
118
  ## `main_stuff` — the output
80
119
 
81
120
  `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.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "TypeScript SDK for the Pipelex hosted API — execute methods, manage runs, and call the product surface.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -61,7 +61,7 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "@eslint/js": "^9.39.4",
64
- "@pipelex/tools-wasm": "0.4.0",
64
+ "@pipelex/tools-wasm": "0.5.0",
65
65
  "@types/node": "^22.12.0",
66
66
  "@vitest/coverage-v8": "^4.1.9",
67
67
  "dependency-cruiser": "^17.4.3",