@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/README.md CHANGED
@@ -134,7 +134,7 @@ try {
134
134
 
135
135
  ### Errors
136
136
 
137
- Every error the SDK throws carries a verdict: `retryable`, whether asking again can succeed, and `errorDomain`, who can fix the failure (`input` for the caller, `config` for a change to the environment such as the credential, the plan or the base URL, `runtime` for nobody beforehand). Both are always decided, and `errorVerdictOf` reads them from anything a `catch` holds, returning `undefined` for what is not an SDK error, such as a bug in the calling code or your own abort. "Retryable" means a retry can succeed, not that it is safe: a start answered with a `500` may already have created a run.
137
+ Every error the SDK throws carries a verdict: `retryable`, whether asking again can succeed, and `errorDomain`, who can fix the failure (`input` for the caller, `config` for a change to the environment such as the credential, the plan or the base URL, `runtime` for nobody beforehand). Both are always decided, and `errorVerdictOf` reads them from anything a `catch` holds, returning `undefined` for what is not an SDK error, such as a bug in the calling code or your own abort. A failed `start` or blocking `execute` also says whether it may have created a run anyway, as `runMayHaveStarted`, read with `runMayHaveStartedOf`: when it is `true`, `retryable` is `false`, since asking again could start a second paid run, and the person should look the run up in the run history first ([`docs/errors.md`](./docs/errors.md#a-failed-run-start--runmayhavestarted)).
138
138
 
139
139
  ```ts
140
140
  import { errorVerdictOf } from "@pipelex/sdk";
@@ -152,7 +152,7 @@ try {
152
152
 
153
153
  A refused request throws an `ApiResponseError` carrying every member of the server's RFC 9457 problem document. **Branch on `errorDomain` and `type`**, as the hosted-envelope spec says, never on the HTTP status or the message: `errorDomain` is the verdict's domain and `type` is the stable URI of the error class, the same on every occurrence. The verdict is the server's when it sent a valid one, and otherwise the SDK's fallback, read from the status, except that an `input` or `config` domain the server sent without `retryable` is not retryable; `problemDocument` keeps what the server sent. `userAction` gives the next step, and `requestId` — from the body, or the `X-Request-ID` header — is the id to hand to support. `code` (the platform's native code, one-to-one with `type`) and `errorType` (the runner's exception class name) are each surface's finer code, and a client of that surface may branch on it too: on the platform's own refusals `code` and `type` read the same answer, `MethodErrorCode` naming the codes a method caller branches on, while a refusal the platform relays from the runner carries the runner's `type` and `errorType` and no `code`. `errors` carries the platform's field-level failures.
154
154
 
155
- On the hosted API, a run that ends without completing throws a `RunFailedError` from `waitForResult`, `startAndWaitForResult` and `downloadArtifacts`, and comes back as the `failed` arm of `getRunResult`. (Against a bare `pipelex-api` runner, `startAndWaitForResult` runs the method with the blocking `execute`, so a failed run there throws the runner's `ApiResponseError`, whose problem members carry the same classification.) Its `status` is the run's terminal status and its `error` is the run's stored error report, checked field by field and typed as `RunErrorReport`: the reason in `message`, `error_domain`, `type_uri` and `retryable` to branch on, `user_action` as the next step, and the inference details. It is `null` when the run ended without a report, and the error's own verdict comes from it: the report's domain, and retryable only when the report says so. The report is the runner's VERBOSE one, provider text included, so what a person sees is your presentation:
155
+ On the hosted API, a run that ends without completing throws a `RunFailedError` from `waitForResult`, `startAndWaitForResult` and `downloadArtifacts`, and comes back as the `failed` arm of `getRunResult`. (Against a bare `pipelex-api` runner, `startAndWaitForResult` runs the method with the blocking `execute`, so a failed run there throws the runner's `ApiResponseError`, whose problem members carry the same classification.) Its `status` is the run's terminal status and its `error` is the run's stored error report, checked field by field and typed as `RunErrorReport`: the reason in `message`, `error_domain`, `type_uri` and `retryable` to branch on, `user_action` as the next step, and the provider's answer in `provider_metadata`'s `status_code` and `retry_after_seconds`. It is `null` when the run ended without a report, and the error's own verdict comes from it: the report's domain, and retryable only when the report says so. The platform serves the runner's STRICT projection of the report: `message` is the author's copy when `caller_facing_message` is `true` and `An internal error occurred.` otherwise, `user_action` comes only with caller-facing copy, and the model, the provider and the provider's own text are left out, so `message` can be shown as it comes and what a person sees is still your presentation:
156
156
 
157
157
  ```ts
158
158
  import { RunFailedError } from "@pipelex/sdk";
package/dist/cli/run.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * as soon as the run exists; and the main output reaches stdout once the run completes, however
9
9
  * long that takes.
10
10
  */
11
- import { ApiResponseError, ApiUnreachableError, MissingMainStuffError, PipelineExecuteTimeoutError, RunFailedError, isGatewayCutOff, renderInputsTemplate, } from "../index.js";
11
+ import { MissingMainStuffError, RunFailedError, renderInputsTemplate, runMayHaveStartedOf, } from "../index.js";
12
12
  import { parseFlags } from "./args.js";
13
13
  import { readBundle } from "./bundle.js";
14
14
  import { makeClient, readPollInterval } from "./environment.js";
@@ -75,7 +75,7 @@ export async function runCommandRun(args, io) {
75
75
  onStarting: () => {
76
76
  // Only once a request that may create a run is about to leave: an interrupt before
77
77
  // it, during the version handshake included, starts nothing, and the command says so.
78
- stage = { kind: "starting", since: performance.now() };
78
+ stage = { kind: "starting" };
79
79
  },
80
80
  onStarted: (ack) => {
81
81
  if (io.interrupt.aborted)
@@ -122,120 +122,17 @@ function waitedOnRun(stage, error) {
122
122
  return undefined;
123
123
  return stage.runId;
124
124
  }
125
- /**
126
- * Whether a transport failure proves the request never left, so that no run was created by it: it
127
- * names a step of the connection's set-up, which no request follows. That is the name lookup or
128
- * the connection itself failing (the system calls `getaddrinfo` and `connect`: an unknown host, a
129
- * refused connection, no route to the host or the network, the system's connect time limit),
130
- * undici's own connect time limit, which covers the TLS handshake, and a server certificate the
131
- * TLS handshake refused. Several addresses each failing so, which Node reports together as an
132
- * `AggregateError`, prove it too. A host that drops packets is a connection that never comes:
133
- * undici gives it up after ten seconds (`UND_ERR_CONNECT_TIMEOUT`), before the time limit of any
134
- * request the command sends, as the Python command meets httpx's connect timeout first and reads
135
- * it the same way.
136
- *
137
- * Anything else may have come once the request had left, the Python command reading the same
138
- * failures the same way. A reset (`ECONNRESET`) comes from a `read` whether the connection dropped
139
- * during the TLS handshake or after the request, and so does an `EHOSTUNREACH` met once connected,
140
- * so neither proves anything; nor does another TLS failure, which may come after the handshake;
141
- * nor does the SDK's own time limit (`ABORT_TIMEOUT`), which fetch cannot place before or after
142
- * the connection was made. Every request the command sends has a limit longer than undici's
143
- * connect limit, so with Node's own dispatcher it runs out only once the request has left.
144
- */
145
- function provesNothingSent(error) {
146
- // The SDK keeps fetch's own failure as `cause`, and undici gives the transport's under it.
147
- const fetchFailure = error.cause;
148
- return fetchFailure instanceof Error && isConnectionSetupFailure(fetchFailure.cause);
149
- }
150
- function isConnectionSetupFailure(failure) {
151
- if (failure instanceof AggregateError) {
152
- return failure.errors.length > 0 && failure.errors.every(isConnectionSetupFailure);
153
- }
154
- if (typeof failure !== "object" || failure === null)
155
- return false;
156
- const { code, syscall } = failure;
157
- if (typeof code === "string" && CONNECTION_SETUP_CODES.has(code))
158
- return true;
159
- return typeof syscall === "string" && CONNECTION_SETUP_CALLS.has(syscall);
160
- }
161
- /** The system calls that resolve the host's name and open the connection. */
162
- const CONNECTION_SETUP_CALLS = new Set(["getaddrinfo", "connect"]);
163
- /**
164
- * The codes only a step of the connection's set-up gives: the name lookup's and the connect call's
165
- * own (set even where Node reports several addresses together, without a system call), undici's
166
- * connect time limit, Node's limit on one attempt among several addresses, Node's own check of the
167
- * host's name, and every code Node gives a server certificate the TLS handshake refused.
168
- */
169
- const CONNECTION_SETUP_CODES = new Set([
170
- "ECONNREFUSED",
171
- "ENOTFOUND",
172
- "EAI_AGAIN",
173
- "UND_ERR_CONNECT_TIMEOUT",
174
- "ERR_SOCKET_CONNECTION_TIMEOUT",
175
- "ERR_TLS_CERT_ALTNAME_INVALID",
176
- "ERR_TLS_CERT_ALTNAME_FORMAT",
177
- // Node names the verdicts of OpenSSL's certificate check from a fixed table, the "X509 certificate
178
- // error codes" of its `tls` docs, and gives every other verdict, such as an unhandled critical
179
- // extension or a CA certificate that may not sign certificates, the code `UNSPECIFIED`. The
180
- // error carries nothing else that marks it, so these codes are the mark: the table as Node 24
181
- // compiles it, in its order, then its default. The Python command reads every
182
- // `ssl.SSLCertVerificationError` the same way.
183
- "UNABLE_TO_GET_ISSUER_CERT",
184
- "UNABLE_TO_GET_CRL",
185
- "UNABLE_TO_DECRYPT_CERT_SIGNATURE",
186
- "UNABLE_TO_DECRYPT_CRL_SIGNATURE",
187
- "UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY",
188
- "CERT_SIGNATURE_FAILURE",
189
- "CRL_SIGNATURE_FAILURE",
190
- "CERT_NOT_YET_VALID",
191
- "CERT_HAS_EXPIRED",
192
- "CRL_NOT_YET_VALID",
193
- "CRL_HAS_EXPIRED",
194
- "ERROR_IN_CERT_NOT_BEFORE_FIELD",
195
- "ERROR_IN_CERT_NOT_AFTER_FIELD",
196
- "ERROR_IN_CRL_LAST_UPDATE_FIELD",
197
- "ERROR_IN_CRL_NEXT_UPDATE_FIELD",
198
- "OUT_OF_MEM",
199
- "DEPTH_ZERO_SELF_SIGNED_CERT",
200
- "SELF_SIGNED_CERT_IN_CHAIN",
201
- "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
202
- "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
203
- "CERT_CHAIN_TOO_LONG",
204
- "CERT_REVOKED",
205
- "INVALID_CA",
206
- "PATH_LENGTH_EXCEEDED",
207
- "INVALID_PURPOSE",
208
- "CERT_UNTRUSTED",
209
- "CERT_REJECTED",
210
- "HOSTNAME_MISMATCH",
211
- "UNSPECIFIED",
212
- ]);
213
125
  /**
214
126
  * Whether a failure met once a request that may create a run was sent, and before the API named
215
- * the run, leaves it unknown whether one was created: its answer was lost to a time limit, a
216
- * connection that closed once the request had left or a gateway that cut the request off, or it
217
- * came back unreadable, or a gateway answered that it lost or never got the server's answer
218
- * (`502`, `504`, RFC 9110). A gateway cuts a request off at ~30 seconds, whether a blocking
219
- * execute or a start the server is still handling, such as one fetching a `method_ref`'s package:
220
- * the SDK's `isGatewayCutOff` tells it, past its threshold, from the time since `onStarting`. Any
221
- * other answer from the API, a `503` that came back before that saying the request was not handled
222
- * included, and a failure that proves nothing was sent, say no run was created.
127
+ * the run, leaves it unknown whether one was created. The SDK decides it on the error it raises
128
+ * (`runMayHaveStarted`): its answer was lost to a time limit, a connection that closed once the
129
+ * request had left or a gateway that cut the request off, it came back unreadable, or the server
130
+ * or a gateway failed while handling it. A failure that proves nothing was sent, and an answer
131
+ * refusing the request before anything ran, say no run was created.
223
132
  */
224
133
  function mayHaveStarted(stage, error) {
225
- if (stage.kind !== "starting")
226
- return false;
227
- if (error instanceof PipelineExecuteTimeoutError)
228
- return true;
229
- if (error instanceof ApiUnreachableError)
230
- return !provesNothingSent(error);
231
- if (!(error instanceof ApiResponseError))
232
- return false;
233
- if (error.status >= 200 && error.status < 300)
234
- return true;
235
- return (GATEWAY_LOST_ANSWER.has(error.status) || isGatewayCutOff(error, performance.now() - stage.since));
134
+ return stage.kind === "starting" && runMayHaveStartedOf(error);
236
135
  }
237
- /** The gateway statuses that say the server's answer was lost or never came (RFC 9110). */
238
- const GATEWAY_LOST_ANSWER = new Set([502, 504]);
239
136
  /** The line under a failure that leaves it unknown whether a run was created. */
240
137
  export const RUN_MAY_HAVE_STARTED_LINE = "A run may have started on the server without the command learning of it, so check before starting it again.";
241
138
  /** The line under a failure met while waiting on a run that exists. */
@@ -1 +1 @@
1
- {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"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"}
@@ -15,7 +15,7 @@ import type { CommandIO } from "./io.js";
15
15
  * update notice and npm 12's run notices off the script's stderr, which carries the command's
16
16
  * own lines, while npm's warnings and errors still print.
17
17
  */
18
- export declare const SCRIPT_RUNNER = "npx --yes --no-update-notifier --loglevel=warn @pipelex/sdk@0.38.0";
18
+ export declare const SCRIPT_RUNNER = "npx --yes --no-update-notifier --loglevel=warn @pipelex/sdk@0.39.0";
19
19
  export declare const SCRIPT_WRITER = "@pipelex/sdk";
20
20
  export declare const SCRIPT_NEEDS = "Node 22.12 or later";
21
21
  /** Run `pipelex-sdk script` and return its exit code. */
package/dist/client.d.ts CHANGED
@@ -161,7 +161,8 @@ export declare class PipelexApiClient implements MTHDSProtocol<DictPipeOutput> {
161
161
  * DNS/connect/TLS/timeout failures as `ApiUnreachableError`; a caller-driven
162
162
  * abort (Ctrl-C / agent walk-away) propagates as-is so the poll loop can stop
163
163
  * cleanly. Non-2xx interpretation is left to the caller. `url` is a fully
164
- * resolved absolute URL.
164
+ * resolved absolute URL. `mayStartRun` marks a request that may create a run,
165
+ * whose `ApiUnreachableError` then says whether it may have (see `RunCreation`).
165
166
  */
166
167
  private requestRaw;
167
168
  /**
@@ -230,6 +231,7 @@ export declare class PipelexApiClient implements MTHDSProtocol<DictPipeOutput> {
230
231
  * `API <method> <path> answered <status> with <what>`: the answer's status, its raw text as
231
232
  * `responseBody`, no problem member but the `X-Request-ID` header, the failure that made it
232
233
  * unreadable as `cause`, and the verdict the fallback gives a 2xx, `runtime` and not retryable.
234
+ * On a request that may create a run, the run may have started: the server accepted it.
233
235
  */
234
236
  private unreadableAnswer;
235
237
  /**
@@ -249,6 +251,11 @@ export declare class PipelexApiClient implements MTHDSProtocol<DictPipeOutput> {
249
251
  * error. Hosted does not emit 202 today, but the protocol permits it;
250
252
  * raising a typed error (with the `pipeline_run_id` + `Location` + `Retry-After`
251
253
  * hints) beats a generic parse failure on an unexpected body shape.
254
+ *
255
+ * The 202 must name the run it accepted, as `start`'s acknowledgement must: one whose body is
256
+ * not JSON, or names no run (an empty id included), leaves the caller a run it cannot follow, so it is an answer the
257
+ * SDK cannot read, which may have started a run, never a `RunStillRunningError` without an id.
258
+ * The Python SDK reads the same answers the same way.
252
259
  */
253
260
  private throwIfExecuteDegraded;
254
261
  /**
@@ -538,12 +545,17 @@ export declare class PipelexApiClient implements MTHDSProtocol<DictPipeOutput> {
538
545
  * Fetch a run's status by bare id — `GET /v1/runs/{pipeline_run_id}/status`.
539
546
  *
540
547
  * Self-healing: a finished-but-unrecorded run resolves to its true terminal
541
- * status on read. `degraded: true` means Temporal was unreachable and
542
- * `status` is the last-known value; `retry_after_seconds` carries the
543
- * server's backoff hint when present. A failed run's stored report, `error`,
544
- * is checked field by field (see `RunErrorReport`). Throws
545
- * `RunLifecycleUnavailableError` when the lifecycle routes are absent (a bare
546
- * runner).
548
+ * status on read. `degraded: true` means the read is not a plain fresh reading,
549
+ * and `degradation.reason` says why (see `DegradationReason`):
550
+ * `status_unavailable` when the platform could not reach the workflow engine
551
+ * and `status` is the last one recorded, `workflow_task_failing` when `status`
552
+ * is live and the run is stuck, its workflow task failing until the platform
553
+ * ends it at `degradation.ends_at`. `retry_after_seconds` carries the server's
554
+ * `Retry-After` hint when present, and `request_id` the id to quote to support.
555
+ * A failed run's stored report, `error`, and the `degradation` are each
556
+ * checked field by field (see `RunErrorReport` and `RunDegradation`), so
557
+ * neither ever fails the read. Throws `RunLifecycleUnavailableError` when the
558
+ * lifecycle routes are absent (a bare runner).
547
559
  */
548
560
  getRunStatus(runId: string, options?: {
549
561
  signal?: AbortSignal;
@@ -609,6 +621,13 @@ export declare class PipelexApiClient implements MTHDSProtocol<DictPipeOutput> {
609
621
  * `pollOptions.signal` is also read before the run is created: a caller that aborts while the
610
622
  * version handshake is in flight gets its abort, and neither the start nor the blocking execute
611
623
  * is sent. Once one of them is sent, the abort stops only the wait.
624
+ *
625
+ * An error from the start, or from the blocking execute, says whether that request may still
626
+ * have started a run whose id never reached the caller (`runMayHaveStarted`); when it may have,
627
+ * it is not retryable, since calling this method again could pay for the run twice: look the run
628
+ * up in the run history first. An error from the wait, once `onStarted` has named the run,
629
+ * started nothing, so its `runMayHaveStarted` is false, and its `retryable` is about waiting
630
+ * again on that id with `waitForResult`, never about calling this method again.
612
631
  */
613
632
  startAndWaitForResult(options: PipelexStartOptions, pollOptions?: StartAndWaitForResultOptions): Promise<RunResults>;
614
633
  /** The authenticated user's profile — `GET /v1/me`. */
@@ -956,6 +975,9 @@ export declare class PipelexApiClient implements MTHDSProtocol<DictPipeOutput> {
956
975
  * answers with, which states the kind of every input at every depth, so a file
957
976
  * position is a fact of the method, never a guess from the value's shape. It
958
977
  * needs an API serving `POST /v1/pipe-io`. See `docs/input-preparation.md`.
978
+ *
979
+ * With `allowUploads: false` it uploads nothing and reads no file: a value that
980
+ * would need an upload throws `UploadNotAllowedError`, naming the input.
959
981
  */
960
982
  prepareInputs(request: PrepareInputsRequest): Promise<PreparedInputs>;
961
983
  /**
@@ -1050,8 +1072,9 @@ export declare class PipelexApiClient implements MTHDSProtocol<DictPipeOutput> {
1050
1072
  * `is_gateway_cut_off` reads the same failures, its `ABORT_TIMEOUT` being httpx's read or write
1051
1073
  * timeout.
1052
1074
  *
1053
- * The blocking `execute` turns such a failure into a `PipelineExecuteTimeoutError`. A caller timing
1054
- * a request that may create a run, such as `start` from `startAndWaitForResult`'s `onStarting`,
1055
- * reads it to know the server may still be handling the request the gateway gave up on.
1075
+ * The blocking `execute` turns such a failure into a `PipelineExecuteTimeoutError`. Whether a
1076
+ * failed `start` or `execute` may have started a run needs no timing: the error says it as
1077
+ * `runMayHaveStarted`, which reads every `503` but the platform's coded one as a run that may
1078
+ * have started, however quickly it came back.
1056
1079
  */
1057
1080
  export declare function isGatewayCutOff(error: unknown, elapsedMs: number): boolean;
package/dist/client.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // source combinations are legal is an invariant of `RunRequest` itself, so this
3
3
  // client and the MTHDS runners cannot disagree about what they reject.
4
4
  import { assertExclusiveRunSources, hasBundlePayload, parseMethodFiles, serializeMethodFiles, } from "mthds/protocol";
5
- import { assertArtifactSelection, assertWaitOptions, pollUntilResult, selectionIncludesMainStuff, throwIfAborted, } from "./runs.js";
5
+ import { assertArtifactSelection, assertWaitOptions, pollUntilResult, readRunDegradation, selectionIncludesMainStuff, throwIfAborted, } from "./runs.js";
6
6
  import { ApiResponseError, ApiUnreachableError, EmptyMethodSourceError, MissingMainStuffError, PagingNotTerminatingError, PipelineExecuteTimeoutError, PipelineRequestError, RequestArgumentError, RunLifecycleUnavailableError, RunStillRunningError, } from "./errors.js";
7
7
  import { isPlainObject, isValidationItem, readFieldError, readMigration, readProviderMetadata, readRunErrorReport, } from "./error-models.js";
8
8
  import { methodSourceToContents } from "./method-source.js";
@@ -29,6 +29,7 @@ const MAX_PAGES = 10_000;
29
29
  /** Decoders of an answer's bytes: the strict one tells a body that is not UTF-8. */
30
30
  const STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true });
31
31
  const LENIENT_UTF8 = new TextDecoder("utf-8");
32
+ const STARTS_RUN = Object.freeze({ mayStartRun: true });
32
33
  /** Hosted default — the client composes every endpoint as `{base}/v1/{endpoint}`. */
33
34
  export const DEFAULT_API_BASE_URL = "https://api.pipelex.com";
34
35
  // The client composes every endpoint from one origin (PIPELEX_BASE_URL): `{base}/v1/{endpoint}`.
@@ -155,7 +156,8 @@ export class PipelexApiClient {
155
156
  * DNS/connect/TLS/timeout failures as `ApiUnreachableError`; a caller-driven
156
157
  * abort (Ctrl-C / agent walk-away) propagates as-is so the poll loop can stop
157
158
  * cleanly. Non-2xx interpretation is left to the caller. `url` is a fully
158
- * resolved absolute URL.
159
+ * resolved absolute URL. `mayStartRun` marks a request that may create a run,
160
+ * whose `ApiUnreachableError` then says whether it may have (see `RunCreation`).
159
161
  */
160
162
  async requestRaw(method, url, options = {}) {
161
163
  const hasBody = options.body !== undefined;
@@ -203,7 +205,7 @@ export class PipelexApiClient {
203
205
  // is classified from the controller rather than from `err`: a browser errors a
204
206
  // body stream cut short by that abort with a generic AbortError instead.
205
207
  const code = extractNetworkErrorCode(controller.signal.aborted ? controller.signal.reason : err);
206
- throw new ApiUnreachableError(`Could not reach Pipelex API at ${this.baseUrl} (${code ?? "network error"})`, this.baseUrl, code, { cause: err });
208
+ throw new ApiUnreachableError(`Could not reach Pipelex API at ${this.baseUrl} (${code ?? "network error"})`, this.baseUrl, code, { cause: err, mayStartRun: options.mayStartRun === true });
207
209
  }
208
210
  finally {
209
211
  clearTimeout(timer);
@@ -306,16 +308,16 @@ export class PipelexApiClient {
306
308
  }
307
309
  return res;
308
310
  }
309
- throwApiResponseError(method, endpoint, res) {
310
- this.throwApiResponseErrorAt(method, `/${API_PREFIX}/${endpoint}`, res);
311
+ throwApiResponseError(method, endpoint, res, run = {}) {
312
+ this.throwApiResponseErrorAt(method, `/${API_PREFIX}/${endpoint}`, res, run);
311
313
  }
312
314
  /** The JSON body of a `/v1` route's 2xx answer; see `readAnswerAt`. */
313
315
  readAnswer(method, endpoint, res) {
314
316
  return this.readAnswerAt(method, `/${API_PREFIX}/${endpoint}`, res);
315
317
  }
316
318
  /** The JSON object body of a `/v1` route's 2xx answer; see `readObjectAnswerAt`. */
317
- readObjectAnswer(method, endpoint, res) {
318
- return this.readObjectAnswerAt(method, `/${API_PREFIX}/${endpoint}`, res);
319
+ readObjectAnswer(method, endpoint, res, run = {}) {
320
+ return this.readObjectAnswerAt(method, `/${API_PREFIX}/${endpoint}`, res, run);
319
321
  }
320
322
  /**
321
323
  * The JSON body of a 2xx answer, naming the route by its `path` from the origin as a refusal
@@ -324,14 +326,15 @@ export class PipelexApiClient {
324
326
  * failure as `cause`.
325
327
  * The one place a success body is parsed.
326
328
  */
327
- readAnswerAt(method, path, res) {
328
- if (!res.utf8)
329
- throw this.unreadableAnswer(method, path, res, "a body that is not UTF-8");
329
+ readAnswerAt(method, path, res, run = {}) {
330
+ if (!res.utf8) {
331
+ throw this.unreadableAnswer(method, path, res, "a body that is not UTF-8", undefined, run);
332
+ }
330
333
  try {
331
334
  return JSON.parse(res.body);
332
335
  }
333
336
  catch (err) {
334
- throw this.unreadableAnswer(method, path, res, res.body ? "a body that is not JSON" : "an empty body where JSON was expected", err);
337
+ throw this.unreadableAnswer(method, path, res, res.body ? "a body that is not JSON" : "an empty body where JSON was expected", err, run);
335
338
  }
336
339
  }
337
340
  /**
@@ -339,10 +342,10 @@ export class PipelexApiClient {
339
342
  * reads it. A body that is JSON but not an object — `null`, a number, a string, an array — is
340
343
  * an answer the SDK cannot read either, and throws the same `ApiResponseError`, with no `cause`.
341
344
  */
342
- readObjectAnswerAt(method, path, res) {
343
- const answer = this.readAnswerAt(method, path, res);
345
+ readObjectAnswerAt(method, path, res, run = {}) {
346
+ const answer = this.readAnswerAt(method, path, res, run);
344
347
  if (!isPlainObject(answer)) {
345
- throw this.unreadableAnswer(method, path, res, "a body that is not an object");
348
+ throw this.unreadableAnswer(method, path, res, "a body that is not an object", undefined, run);
346
349
  }
347
350
  return answer;
348
351
  }
@@ -351,20 +354,29 @@ export class PipelexApiClient {
351
354
  * `API <method> <path> answered <status> with <what>`: the answer's status, its raw text as
352
355
  * `responseBody`, no problem member but the `X-Request-ID` header, the failure that made it
353
356
  * unreadable as `cause`, and the verdict the fallback gives a 2xx, `runtime` and not retryable.
357
+ * On a request that may create a run, the run may have started: the server accepted it.
354
358
  */
355
- unreadableAnswer(method, path, res, what, cause) {
356
- return new ApiResponseError(`API ${method} ${path} answered ${res.status} with ${what}`, this.baseUrl, res.status, res.statusText, res.body, undefined, undefined, undefined, undefined, { cause, problem: { requestId: nonEmptyHeader(res.headers, REQUEST_ID_HEADER) } });
359
+ unreadableAnswer(method, path, res, what, cause, run = {}) {
360
+ return new ApiResponseError(`API ${method} ${path} answered ${res.status} with ${what}`, this.baseUrl, res.status, res.statusText, res.body, undefined, undefined, undefined, undefined, {
361
+ cause,
362
+ problem: { requestId: nonEmptyHeader(res.headers, REQUEST_ID_HEADER) },
363
+ mayStartRun: run.mayStartRun === true,
364
+ });
357
365
  }
358
366
  /**
359
367
  * Throw the `ApiResponseError` of a non-2xx answer, naming the route by its `path` from the
360
368
  * origin (`/v1/...`, or `/health` for the liveness probe).
361
369
  */
362
- throwApiResponseErrorAt(method, path, res) {
370
+ throwApiResponseErrorAt(method, path, res, run = {}) {
363
371
  const { errorType, serverMessage, validationErrors, code, problem, document } = parseErrorBody(res.body);
364
372
  // The body's `request_id` wins; the header is the fallback for a response whose body
365
373
  // carries none (a gateway error page, a non-problem body).
366
374
  const requestId = problem.requestId ?? nonEmptyHeader(res.headers, REQUEST_ID_HEADER);
367
- throw new ApiResponseError(`API ${method} ${path} failed (${res.status}): ${serverMessage ?? (res.body || res.statusText)}`, this.baseUrl, res.status, res.statusText, res.body, errorType, serverMessage, validationErrors, code, { problem: { ...problem, requestId }, problemDocument: document });
375
+ throw new ApiResponseError(`API ${method} ${path} failed (${res.status}): ${serverMessage ?? (res.body || res.statusText)}`, this.baseUrl, res.status, res.statusText, res.body, errorType, serverMessage, validationErrors, code, {
376
+ problem: { ...problem, requestId },
377
+ problemDocument: document,
378
+ mayStartRun: run.mayStartRun === true,
379
+ });
368
380
  }
369
381
  /**
370
382
  * Translate a "route absent" 404 (a bare pipelex-api with no platform block)
@@ -386,23 +398,21 @@ export class PipelexApiClient {
386
398
  * error. Hosted does not emit 202 today, but the protocol permits it;
387
399
  * raising a typed error (with the `pipeline_run_id` + `Location` + `Retry-After`
388
400
  * hints) beats a generic parse failure on an unexpected body shape.
401
+ *
402
+ * The 202 must name the run it accepted, as `start`'s acknowledgement must: one whose body is
403
+ * not JSON, or names no run (an empty id included), leaves the caller a run it cannot follow, so it is an answer the
404
+ * SDK cannot read, which may have started a run, never a `RunStillRunningError` without an id.
405
+ * The Python SDK reads the same answers the same way.
389
406
  */
390
407
  throwIfExecuteDegraded(res) {
391
408
  if (res.status !== 202)
392
409
  return;
393
- let runId = "";
394
- try {
395
- const parsed = JSON.parse(res.body);
396
- if (parsed && typeof parsed === "object") {
397
- const candidate = parsed.pipeline_run_id;
398
- if (typeof candidate === "string")
399
- runId = candidate;
400
- }
401
- }
402
- catch {
403
- // Non-JSON 202 body — keep runId empty; the error message covers it.
410
+ const ack = this.readObjectAnswer("POST", "execute", res, STARTS_RUN);
411
+ const runId = ack.pipeline_run_id;
412
+ if (typeof runId !== "string" || runId === "") {
413
+ throw this.unreadableAnswer("POST", `/${API_PREFIX}/execute`, res, "a body that is not the answer the route returns", undefined, STARTS_RUN);
404
414
  }
405
- throw new RunStillRunningError(`execute() was accepted asynchronously (202): run ${runId || "<unknown>"} is still ` +
415
+ throw new RunStillRunningError(`execute() was accepted asynchronously (202): run ${runId} is still ` +
406
416
  "running server-side. Poll its results (hosted) or use start().", runId, parseRetryAfter(res.headers), res.headers.get("location"));
407
417
  }
408
418
  // ── Health ────────────────────────────────────────────────────────
@@ -472,14 +482,19 @@ export class PipelexApiClient {
472
482
  try {
473
483
  const res = await this.requestRaw("POST", this.url("execute"), {
474
484
  body: request,
485
+ ...STARTS_RUN,
475
486
  });
476
487
  this.throwIfExecuteDegraded(res);
477
488
  if (res.status < 200 || res.status >= 300) {
478
- this.throwApiResponseError("POST", "execute", res);
489
+ this.throwApiResponseError("POST", "execute", res, STARTS_RUN);
479
490
  }
480
491
  // Wrap the base result in the enriched subtype (adds the `.main_stuff` accessor; the
481
492
  // `main_stuff_name` extension + working memory ride `pipe_output`).
482
- return new PipelexExecuteResult(this.readObjectAnswer("POST", "execute", res));
493
+ const answer = this.readObjectAnswer("POST", "execute", res, STARTS_RUN);
494
+ if (!isCompletedRunAnswer(answer)) {
495
+ throw this.unreadableAnswer("POST", `/${API_PREFIX}/execute`, res, "a body that is not the answer the route returns", undefined, STARTS_RUN);
496
+ }
497
+ return new PipelexExecuteResult(answer);
483
498
  }
484
499
  catch (err) {
485
500
  if (err instanceof RunStillRunningError)
@@ -553,15 +568,23 @@ export class PipelexApiClient {
553
568
  const res = await this.requestRaw("POST", url, {
554
569
  body: request,
555
570
  timeoutMs: needsLongCeiling ? DEFAULT_REQUEST_TIMEOUT_MS : POLL_REQUEST_TIMEOUT_MS,
571
+ ...STARTS_RUN,
556
572
  });
557
573
  // A bare runner with no run store 404s here just as it does on the result
558
574
  // routes — surface the same clear `RunLifecycleUnavailableError` (and let
559
575
  // `startAndWaitForResult` fall back to the blocking `execute`).
560
576
  this.throwIfLifecycleUnavailable(res, url);
561
577
  if (res.status < 200 || res.status >= 300) {
562
- this.throwApiResponseError("POST", "start", res);
578
+ this.throwApiResponseError("POST", "start", res, STARTS_RUN);
579
+ }
580
+ const ack = this.readObjectAnswer("POST", "start", res, STARTS_RUN);
581
+ // The acknowledgement's one job is to name the run: without its id, the run the server
582
+ // accepted is lost to the caller, as surely as behind a body that is not JSON, and an empty id
583
+ // names no run either. Worded as the Python SDK words an answer its model refuses.
584
+ if (typeof ack.pipeline_run_id !== "string" || ack.pipeline_run_id === "") {
585
+ throw this.unreadableAnswer("POST", `/${API_PREFIX}/start`, res, "a body that is not the answer the route returns", undefined, STARTS_RUN);
563
586
  }
564
- return this.readObjectAnswer("POST", "start", res);
587
+ return ack;
565
588
  }
566
589
  /**
567
590
  * Parse, validate, and dry-run an MTHDS bundle — `POST /v1/validate`.
@@ -971,12 +994,17 @@ export class PipelexApiClient {
971
994
  * Fetch a run's status by bare id — `GET /v1/runs/{pipeline_run_id}/status`.
972
995
  *
973
996
  * Self-healing: a finished-but-unrecorded run resolves to its true terminal
974
- * status on read. `degraded: true` means Temporal was unreachable and
975
- * `status` is the last-known value; `retry_after_seconds` carries the
976
- * server's backoff hint when present. A failed run's stored report, `error`,
977
- * is checked field by field (see `RunErrorReport`). Throws
978
- * `RunLifecycleUnavailableError` when the lifecycle routes are absent (a bare
979
- * runner).
997
+ * status on read. `degraded: true` means the read is not a plain fresh reading,
998
+ * and `degradation.reason` says why (see `DegradationReason`):
999
+ * `status_unavailable` when the platform could not reach the workflow engine
1000
+ * and `status` is the last one recorded, `workflow_task_failing` when `status`
1001
+ * is live and the run is stuck, its workflow task failing until the platform
1002
+ * ends it at `degradation.ends_at`. `retry_after_seconds` carries the server's
1003
+ * `Retry-After` hint when present, and `request_id` the id to quote to support.
1004
+ * A failed run's stored report, `error`, and the `degradation` are each
1005
+ * checked field by field (see `RunErrorReport` and `RunDegradation`), so
1006
+ * neither ever fails the read. Throws `RunLifecycleUnavailableError` when the
1007
+ * lifecycle routes are absent (a bare runner).
980
1008
  */
981
1009
  async getRunStatus(runId, options = {}) {
982
1010
  const endpoint = `${RUNS}/${encodeURIComponent(runId)}/status`;
@@ -989,7 +1017,7 @@ export class PipelexApiClient {
989
1017
  if (res.status < 200 || res.status >= 300) {
990
1018
  this.throwApiResponseError("GET", endpoint, res);
991
1019
  }
992
- const run = withCheckedReport(this.readObjectAnswer("GET", endpoint, res));
1020
+ const run = withCheckedDegradation(withCheckedReport(this.readObjectAnswer("GET", endpoint, res)));
993
1021
  const retryAfter = parseRetryAfter(res.headers);
994
1022
  return retryAfter !== null ? { ...run, retry_after_seconds: retryAfter } : run;
995
1023
  }
@@ -1104,6 +1132,13 @@ export class PipelexApiClient {
1104
1132
  * `pollOptions.signal` is also read before the run is created: a caller that aborts while the
1105
1133
  * version handshake is in flight gets its abort, and neither the start nor the blocking execute
1106
1134
  * is sent. Once one of them is sent, the abort stops only the wait.
1135
+ *
1136
+ * An error from the start, or from the blocking execute, says whether that request may still
1137
+ * have started a run whose id never reached the caller (`runMayHaveStarted`); when it may have,
1138
+ * it is not retryable, since calling this method again could pay for the run twice: look the run
1139
+ * up in the run history first. An error from the wait, once `onStarted` has named the run,
1140
+ * started nothing, so its `runMayHaveStarted` is false, and its `retryable` is about waiting
1141
+ * again on that id with `waitForResult`, never about calling this method again.
1107
1142
  */
1108
1143
  async startAndWaitForResult(options, pollOptions) {
1109
1144
  // Before the run starts: a refusal after it would carry no run id to re-poll by.
@@ -1632,6 +1667,9 @@ export class PipelexApiClient {
1632
1667
  * answers with, which states the kind of every input at every depth, so a file
1633
1668
  * position is a fact of the method, never a guess from the value's shape. It
1634
1669
  * needs an API serving `POST /v1/pipe-io`. See `docs/input-preparation.md`.
1670
+ *
1671
+ * With `allowUploads: false` it uploads nothing and reads no file: a value that
1672
+ * would need an upload throws `UploadNotAllowedError`, naming the input.
1635
1673
  */
1636
1674
  async prepareInputs(request) {
1637
1675
  return prepareInputsImpl(this, request);
@@ -2063,9 +2101,10 @@ const GATEWAY_TIMEOUT_THRESHOLD_MS = 28_000;
2063
2101
  * `is_gateway_cut_off` reads the same failures, its `ABORT_TIMEOUT` being httpx's read or write
2064
2102
  * timeout.
2065
2103
  *
2066
- * The blocking `execute` turns such a failure into a `PipelineExecuteTimeoutError`. A caller timing
2067
- * a request that may create a run, such as `start` from `startAndWaitForResult`'s `onStarting`,
2068
- * reads it to know the server may still be handling the request the gateway gave up on.
2104
+ * The blocking `execute` turns such a failure into a `PipelineExecuteTimeoutError`. Whether a
2105
+ * failed `start` or `execute` may have started a run needs no timing: the error says it as
2106
+ * `runMayHaveStarted`, which reads every `503` but the platform's coded one as a run that may
2107
+ * have started, however quickly it came back.
2069
2108
  */
2070
2109
  export function isGatewayCutOff(error, elapsedMs) {
2071
2110
  if (elapsedMs < GATEWAY_TIMEOUT_THRESHOLD_MS)
@@ -2125,6 +2164,25 @@ function isMissingRoute404(body) {
2125
2164
  return true;
2126
2165
  return !("code" in parsed);
2127
2166
  }
2167
+ /**
2168
+ * Whether a 2xx answer to `execute` is the completed run the route returns, as far as the SDK reads
2169
+ * it: a string `pipeline_run_id`, and a `pipe_output` object whose `working_memory` carries its
2170
+ * `root` and `aliases` objects and whose own `pipeline_run_id` is a string, the envelope the Python
2171
+ * SDK's model of the answer requires. Anything less is an answer the SDK cannot read, rather than
2172
+ * a result that fails with a bare `TypeError` once its output is lifted. The stuffs inside `root`,
2173
+ * which the Python model also checks, are left to the read of the main stuff, whose
2174
+ * `MissingMainStuffError` names the run.
2175
+ */
2176
+ function isCompletedRunAnswer(answer) {
2177
+ const { pipeline_run_id: runId, pipe_output: pipeOutput } = answer;
2178
+ if (typeof runId !== "string" || !isPlainObject(pipeOutput))
2179
+ return false;
2180
+ const workingMemory = pipeOutput["working_memory"];
2181
+ return (typeof pipeOutput["pipeline_run_id"] === "string" &&
2182
+ isPlainObject(workingMemory) &&
2183
+ isPlainObject(workingMemory["root"]) &&
2184
+ isPlainObject(workingMemory["aliases"]));
2185
+ }
2128
2186
  /** Parse the `Retry-After` header (seconds form, which the platform uses). */
2129
2187
  function parseRetryAfter(headers) {
2130
2188
  const raw = headers.get("retry-after");
@@ -2181,6 +2239,15 @@ function withCheckedReport(record) {
2181
2239
  return record;
2182
2240
  return { ...record, error: readRunErrorReport(record.error) };
2183
2241
  }
2242
+ /**
2243
+ * A status read with its `degradation` checked by `readRunDegradation`. Only when the read carries
2244
+ * the key, so a platform that predates it still leaves it absent; `degraded` is left as sent.
2245
+ */
2246
+ function withCheckedDegradation(run) {
2247
+ if (!Object.hasOwn(run, "degradation"))
2248
+ return run;
2249
+ return { ...run, degradation: readRunDegradation(run.degradation) };
2250
+ }
2184
2251
  function knownRunStatus(value) {
2185
2252
  return typeof value === "string" && KNOWN_RUN_STATUSES.includes(value)
2186
2253
  ? value