@pipelex/sdk 0.33.0 → 0.34.1

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.
Files changed (75) hide show
  1. package/README.md +40 -2
  2. package/dist/cli/args.d.ts +41 -0
  3. package/dist/cli/args.js +96 -0
  4. package/dist/cli/args.js.map +1 -0
  5. package/dist/cli/bundle.d.ts +45 -0
  6. package/dist/cli/bundle.js +148 -0
  7. package/dist/cli/bundle.js.map +1 -0
  8. package/dist/cli/environment.d.ts +36 -0
  9. package/dist/cli/environment.js +72 -0
  10. package/dist/cli/environment.js.map +1 -0
  11. package/dist/cli/help.d.ts +4 -0
  12. package/dist/cli/help.js +56 -0
  13. package/dist/cli/help.js.map +1 -0
  14. package/dist/cli/inputs.d.ts +13 -0
  15. package/dist/cli/inputs.js +70 -0
  16. package/dist/cli/inputs.js.map +1 -0
  17. package/dist/cli/io.d.ts +79 -0
  18. package/dist/cli/io.js +96 -0
  19. package/dist/cli/io.js.map +1 -0
  20. package/dist/cli/main.d.ts +18 -0
  21. package/dist/cli/main.js +58 -0
  22. package/dist/cli/main.js.map +1 -0
  23. package/dist/cli/method.d.ts +56 -0
  24. package/dist/cli/method.js +108 -0
  25. package/dist/cli/method.js.map +1 -0
  26. package/dist/cli/present.d.ts +43 -0
  27. package/dist/cli/present.js +191 -0
  28. package/dist/cli/present.js.map +1 -0
  29. package/dist/cli/run.d.ts +17 -0
  30. package/dist/cli/run.js +290 -0
  31. package/dist/cli/run.js.map +1 -0
  32. package/dist/cli/script.d.ts +23 -0
  33. package/dist/cli/script.js +212 -0
  34. package/dist/cli/script.js.map +1 -0
  35. package/dist/cli/source.d.ts +49 -0
  36. package/dist/cli/source.js +71 -0
  37. package/dist/cli/source.js.map +1 -0
  38. package/dist/cli.d.ts +10 -0
  39. package/dist/cli.js +65 -0
  40. package/dist/cli.js.map +1 -0
  41. package/dist/client.d.ts +70 -8
  42. package/dist/client.js +185 -26
  43. package/dist/client.js.map +1 -1
  44. package/dist/errors.d.ts +42 -4
  45. package/dist/errors.js +43 -4
  46. package/dist/errors.js.map +1 -1
  47. package/dist/hooks/validate-client.d.ts +3 -3
  48. package/dist/hooks/validate-client.js +3 -3
  49. package/dist/index.d.ts +4 -4
  50. package/dist/index.js +2 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/models.d.ts +118 -3
  53. package/dist/models.js +4 -3
  54. package/dist/models.js.map +1 -1
  55. package/dist/prepare-inputs.d.ts +6 -0
  56. package/dist/prepare-inputs.js +21 -10
  57. package/dist/prepare-inputs.js.map +1 -1
  58. package/dist/runs.d.ts +28 -1
  59. package/dist/runs.js +2 -1
  60. package/dist/runs.js.map +1 -1
  61. package/dist/upload.d.ts +6 -0
  62. package/dist/upload.js +9 -4
  63. package/dist/upload.js.map +1 -1
  64. package/dist/user-agent.d.ts +3 -2
  65. package/dist/user-agent.js +3 -2
  66. package/dist/user-agent.js.map +1 -1
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/docs/architecture.md +12 -4
  70. package/docs/cli.md +123 -0
  71. package/docs/client-identification.md +1 -1
  72. package/docs/errors.md +5 -3
  73. package/docs/input-preparation.md +6 -2
  74. package/docs/run-results.md +20 -0
  75. package/package.json +5 -1
@@ -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 workspace spec `docs/specs/client-identification.md`; this page describes how this SDK implements it.
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 workspace's hosted-envelope spec names. The types live in `src/error-models.ts`; `pipelex-sdk` (Python) carries the same fields under snake_case names.
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 (`is_valid: false`, carrying the first validation item's message, else the answer's own `message`); 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;
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
- A malformed data URL — no comma, bad base64, bad percent-encoding — is a plain `InputPreparationError`, raised while the asset is read and before anything is uploaded.
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
 
@@ -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.33.0",
3
+ "version": "0.34.1",
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": {