@pipelex/sdk 0.51.0 → 0.53.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.
Files changed (88) hide show
  1. package/README.md +20 -19
  2. package/dist/answer-reading.d.ts +70 -0
  3. package/dist/answer-reading.js +106 -0
  4. package/dist/answer-reading.js.map +1 -0
  5. package/dist/artifacts.js +45 -32
  6. package/dist/artifacts.js.map +1 -1
  7. package/dist/cli/present.d.ts +11 -14
  8. package/dist/cli/present.js +41 -37
  9. package/dist/cli/present.js.map +1 -1
  10. package/dist/cli/run.js +8 -7
  11. package/dist/cli/run.js.map +1 -1
  12. package/dist/cli/script.d.ts +1 -1
  13. package/dist/cli/source.js +2 -2
  14. package/dist/cli/source.js.map +1 -1
  15. package/dist/client.d.ts +254 -213
  16. package/dist/client.js +542 -472
  17. package/dist/client.js.map +1 -1
  18. package/dist/codegen-check.d.ts +21 -15
  19. package/dist/codegen-check.js +54 -27
  20. package/dist/codegen-check.js.map +1 -1
  21. package/dist/errors.d.ts +298 -455
  22. package/dist/errors.js +426 -534
  23. package/dist/errors.js.map +1 -1
  24. package/dist/exact-json.d.ts +8 -12
  25. package/dist/exact-json.js +19 -12
  26. package/dist/exact-json.js.map +1 -1
  27. package/dist/hooks/check-core.d.ts +12 -7
  28. package/dist/hooks/check-core.js +20 -14
  29. package/dist/hooks/check-core.js.map +1 -1
  30. package/dist/hooks/validate-stage.js +12 -3
  31. package/dist/hooks/validate-stage.js.map +1 -1
  32. package/dist/index.d.ts +4 -5
  33. package/dist/index.js +7 -6
  34. package/dist/index.js.map +1 -1
  35. package/dist/method-selector.js +7 -5
  36. package/dist/method-selector.js.map +1 -1
  37. package/dist/method-trees.d.ts +6 -7
  38. package/dist/method-trees.js +31 -37
  39. package/dist/method-trees.js.map +1 -1
  40. package/dist/models.d.ts +33 -210
  41. package/dist/models.js +2 -1
  42. package/dist/models.js.map +1 -1
  43. package/dist/prepare-inputs.d.ts +2 -2
  44. package/dist/prepare-inputs.js +51 -54
  45. package/dist/prepare-inputs.js.map +1 -1
  46. package/dist/problem-items.d.ts +19 -0
  47. package/dist/problem-items.js +52 -0
  48. package/dist/problem-items.js.map +1 -0
  49. package/dist/product-models.d.ts +43 -29
  50. package/dist/records.d.ts +12 -0
  51. package/dist/records.js +23 -0
  52. package/dist/records.js.map +1 -0
  53. package/dist/runs.d.ts +49 -38
  54. package/dist/runs.js +45 -19
  55. package/dist/runs.js.map +1 -1
  56. package/dist/transport-reading.d.ts +18 -0
  57. package/dist/{run-may-have-started.js → transport-reading.js} +15 -44
  58. package/dist/transport-reading.js.map +1 -0
  59. package/dist/upload-grant.d.ts +9 -6
  60. package/dist/upload-grant.js +42 -29
  61. package/dist/upload-grant.js.map +1 -1
  62. package/dist/upload.js +53 -33
  63. package/dist/upload.js.map +1 -1
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/docs/architecture.md +19 -19
  67. package/docs/artifact-download.md +6 -6
  68. package/docs/cli.md +1 -1
  69. package/docs/crate-routes.md +13 -14
  70. package/docs/errors.md +154 -267
  71. package/docs/exact-integers.md +1 -1
  72. package/docs/files.md +13 -13
  73. package/docs/hook-bundle.md +15 -1
  74. package/docs/input-preparation.md +14 -14
  75. package/docs/method-copy.md +35 -33
  76. package/docs/method-examples.md +13 -14
  77. package/docs/method-trees.md +27 -28
  78. package/docs/organization-branding.md +14 -16
  79. package/docs/run-results.md +8 -8
  80. package/package.json +1 -1
  81. package/dist/error-models.d.ts +0 -302
  82. package/dist/error-models.js +0 -203
  83. package/dist/error-models.js.map +0 -1
  84. package/dist/error-verdicts.d.ts +0 -46
  85. package/dist/error-verdicts.js +0 -120
  86. package/dist/error-verdicts.js.map +0 -1
  87. package/dist/run-may-have-started.d.ts +0 -31
  88. package/dist/run-may-have-started.js.map +0 -1
package/README.md CHANGED
@@ -81,17 +81,17 @@ console.log(saved.method_version); // 3
81
81
  `start` and `startAndWaitForResult` take an optional `idempotencyKey`, sent as the `Idempotency-Key` header of `POST /v1/start`. The hosted API binds the key to the run the first request starts, so the same start sent again under the same key is answered with that run and starts nothing: a start whose answer was lost can be sent again without paying for a second run. Generate one key per run, and keep it with the options, unchanged, across that start's retries:
82
82
 
83
83
  ```ts
84
- import { runMayHaveStartedOf } from "@pipelex/sdk";
84
+ import { isProblemError } from "mthds/protocol";
85
85
 
86
86
  const options = { method_id: "mt_abc123", inputs, idempotencyKey: crypto.randomUUID() };
87
87
  const ack = await client.start(options).catch((err: unknown) => {
88
88
  // The run may exist: under the same key, the same start is answered with it.
89
- if (runMayHaveStartedOf(err)) return client.start(options);
89
+ if (isProblemError(err) && err.retry === "outcome_unknown") return client.start(options);
90
90
  throw err;
91
91
  });
92
92
  ```
93
93
 
94
- A key is 1 to 255 visible ASCII characters; a key reused with other options is refused with a `409` `conflict`, and one whose earlier start is still being handled with a retryable `409` `idempotency_key_in_use` ([`docs/errors.md`](./docs/errors.md#a-starts-idempotency-key--idempotencykey)). A bare runner ignores the header.
94
+ A key is 1 to 255 visible ASCII characters; a key reused with other options is refused with a `409` `request.idempotency_conflict`, and one whose earlier start is still being handled with a `409` `request.idempotency_key_in_use` whose `retry` is `yes` ([`docs/errors.md`](./docs/errors.md#a-starts-idempotency-key--idempotencykey)). A bare runner ignores the header.
95
95
 
96
96
  ### Checking a model reference
97
97
 
@@ -110,7 +110,7 @@ if (verdict.resolution === "resolved") {
110
110
  }
111
111
  ```
112
112
 
113
- A reference the runner cannot read at all (blank, a sigil alone, too long) or an unknown category throws `ApiResponseError`, a `422` whose `errorType` is `InvalidModelReference` or `InvalidModelCategory`.
113
+ A reference the runner cannot read at all (blank, a sigil alone, too long) or an unknown category throws `ApiResponseError`, a `422` `request.invalid` whose `location.field` names `reference` or `type`.
114
114
 
115
115
  ### Product routes
116
116
 
@@ -168,8 +168,9 @@ for await (const saved of client.iterateMethodExamples(created.method_id)) {
168
168
  }
169
169
 
170
170
  // Copy a method into a team, or into your personal workspace, with its draft, its form inputs and
171
- // its examples (web sessions only). A bundle naming storage the destination cannot read answers
172
- // `refused`, a verdict rather than an error; the key makes a retry copy once.
171
+ // its examples (web sessions only). A bundle naming storage the destination cannot read, or, in a
172
+ // duplicate, a file under the original's run files, which erasing the original would remove,
173
+ // answers `refused`, a verdict rather than an error; the key makes a retry copy once.
173
174
  const copy = await client.copyMethod(
174
175
  created.method_id,
175
176
  { destination_org_id: "org_team", expected_draft_updated_at: draft.updated_at },
@@ -207,25 +208,25 @@ try {
207
208
 
208
209
  ### Errors
209
210
 
210
- 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, an error of the standard's own `mthds` client included, which it reads by its problem's `domain` and `retry`, returning `undefined` for what carries no verdict, 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)), unless the start carried an `idempotencyKey`, in which case sending it again unchanged recovers the run ([above](#a-start-you-can-retry-idempotencykey)).
211
+ Every error the SDK throws carries its failure as one problem document, the MTHDS standard's RFC 9457 document, under a registered code: the hosted API's own when it refused the request, the problem the platform stored when a run failed, and one the SDK builds under a `client.*` code for a failure it detects itself, such as a request that got no answer or an answer it cannot read. Every error is the standard's `ProblemError`, so `isProblemError` from `mthds/protocol` matches it, and its getters read the problem: `code` to branch on, `domain` for who must act (`input`, `method`, `account`, `operator` or `runtime`), `retry` for whether sending the same request again can succeed (`yes`, `no`, or `outcome_unknown` when a start may have created a run, which is looked up in the run history before anything is sent again, unless the start carried an `idempotencyKey`, in which case sending it again unchanged recovers the run, [above](#a-start-you-can-retry-idempotencykey)), `nextStep` for what to do, `retryAfter` and `requestId`, the id to hand to support. Branch on the problem, never on the HTTP status or the message.
211
212
 
212
213
  ```ts
213
- import { errorVerdictOf } from "@pipelex/sdk";
214
+ import { isProblemError } from "mthds/protocol";
214
215
 
215
216
  try {
216
217
  await client.startAndWaitForResult({ method_id: "mt_abc123", inputs });
217
218
  } catch (err) {
218
- const verdict = errorVerdictOf(err);
219
- if (verdict === undefined) throw err; // not an SDK error: a bug, or the caller's own abort
220
- if (verdict.retryable) return scheduleRetry();
221
- if (verdict.errorDomain === "input") return askTheUserToFix(err);
222
- return reportToOperator(err);
219
+ if (!isProblemError(err)) throw err; // not an SDK failure: a bug, or the caller's own abort
220
+ if (err.retry === "outcome_unknown") return lookUpTheRunBeforeStartingAgain(err);
221
+ if (err.retry === "yes") return scheduleRetry(err.retryAfter);
222
+ if (err.domain === "input" || err.domain === "method") return askTheUserToFix(err.problem);
223
+ return reportToOperator(err.problem);
223
224
  }
224
225
  ```
225
226
 
226
- 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.
227
+ A refused request throws an `ApiResponseError` whose `problem` is the hosted API's problem document as received, a `pipelex.*` code and every member the standard does not name included, such as the `limit` a refusal past a bound names; an answer whose body is no problem document, from the hosted edge, a gateway or a proxy, reads by its status. `status`, `statusText` and `responseBody` keep the answer itself. `MethodErrorCode` names the codes a method caller branches on, and `IdempotencyErrorCode` those of an `Idempotency-Key`.
227
228
 
228
- 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:
229
+ 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`. Its `problem` is `run.failed`, naming the run and its terminal status, and its `runError` is the run's own problem, which `run.failed` nests as `run_error`: why the run did not succeed, under its own code (`run.cancelled`, `model.unavailable`, `method.invalid`, …), with its own `retry` and `next_step`. It is `undefined` when the run ended with no problem stored.
229
230
 
230
231
  ```ts
231
232
  import { RunFailedError } from "@pipelex/sdk";
@@ -234,13 +235,13 @@ try {
234
235
  await client.waitForResult(runId);
235
236
  } catch (err) {
236
237
  if (err instanceof RunFailedError) {
237
- console.error(err.message); // "Run finished with status FAILED: <the reason>"
238
- console.error(err.error?.user_action?.detail ?? "No next step was given.");
238
+ console.error(err.message); // names the run and its terminal status
239
+ console.error(err.runError?.next_step.detail ?? "No next step was given.");
239
240
  }
240
241
  }
241
242
  ```
242
243
 
243
- [`docs/errors.md`](./docs/errors.md) gives each class's verdict, the fallback table, and every field of both.
244
+ [`docs/errors.md`](./docs/errors.md) gives each class's problem, the reading of an answer that is no problem document, and the case file both SDKs run.
244
245
 
245
246
  ### Client identification
246
247
 
@@ -289,7 +290,7 @@ These pages ship inside the published package, so a reader who has only installe
289
290
  | [`docs/exact-integers.md`](./docs/exact-integers.md) | Which reads keep an integer past `Number.MAX_SAFE_INTEGER` exact, as a `bigint` (a run's inputs and outputs, an example's inputs), how a request body writes it back with its digits, and `parseExactJson` and `stringifyExactJson` for code showing such data as JSON text |
290
291
  | [`docs/crate-routes.md`](./docs/crate-routes.md) | `resolve`, `codegen` and `pipeIo` — the method's tree as the protocol's `files` map, with its dependencies, the normalized crate, the stamped types, and a method's I/O artifacts without a validation — and the offline `runCodegenCheck` that guards a committed tree |
291
292
  | [`docs/client-identification.md`](./docs/client-identification.md) | The `User-Agent` every API request carries, and `appInfo`, the option that puts your program's name in front of it |
292
- | [`docs/errors.md`](./docs/errors.md) | The verdict every error carries (`retryable`, `errorDomain`, `errorVerdictOf`), a failed run's stored error report on `RunFailedError` and `RunRead`, and every member of a refused request's `ApiResponseError`, with the fields to branch on |
293
+ | [`docs/errors.md`](./docs/errors.md) | The problem every error carries (`code`, `domain`, `retry`, `nextStep`), a start whose outcome is unknown, each class's problem, a refused request's `ApiResponseError`, a failed run's `run.failed` and its nested `run_error`, and a verdict's problem items |
293
294
 
294
295
  ## Develop
295
296
 
@@ -0,0 +1,70 @@
1
+ /**
2
+ * How the SDK reads an answer that is not the route's result as a problem: a refusal, a failure, or
3
+ * a `2xx` it cannot read. The Python twin, `pipelex-sdk`, reads every answer the same way in
4
+ * `pipelex_sdk.answer_reading`, and the shared case file `tests/fixtures/sdk-problems.json` pins the
5
+ * readings both apply.
6
+ *
7
+ * - A body that is a problem document reads as received, every member it carries kept, a
8
+ * `pipelex.*` code and the members the standard does not name included, its `status` taken from
9
+ * the answer when it leaves it out and its `retry_after` from the `Retry-After` header when it
10
+ * gives none and its `retry` is `yes` (`readAnswer` in `mthds/protocol`). On a start or an
11
+ * execute too: the problem's own `retry` says whether a run may exist.
12
+ * - A body that is no problem document is the hosted edge's, a gateway's or a proxy's, which
13
+ * nobody configures, and reads by the rules below. They stay written here until the registry
14
+ * records them and every client generates them.
15
+ *
16
+ * On a request that creates a run, a `start` or an `execute`:
17
+ *
18
+ * - a `2xx` the SDK could not read is `client.outcome_unknown`, since the server accepted the
19
+ * request and only its acknowledgement was lost, naming the run when the body names one;
20
+ * - a `408`, which a proxy may answer for a request it forwarded, and a `5xx` but a `501`, which a
21
+ * gateway answers when the platform was slow or unreachable behind it, are
22
+ * `client.outcome_unknown` too, whatever the time it took: the run may have started behind it.
23
+ *
24
+ * On any route, and on a start for every other status:
25
+ *
26
+ * - a `401` is `auth.missing_credential`: the edge answers it before the platform when the request
27
+ * carries no credential;
28
+ * - a `403` is `auth.invalid_credential`: the edge's authorizer refused the credential, or the
29
+ * route is outside what the credential may call;
30
+ * - a `404` is `request.not_found`: no route matches;
31
+ * - a `408` and a `429` are `runtime.unavailable`, with the `Retry-After` the answer gives: a
32
+ * proxy timed the request out, or the edge throttled it, and a later attempt can pass. The core
33
+ * registry's `account.rate_limited` names the limit and its window, which an edge throttle does
34
+ * not say, so a client cannot state it;
35
+ * - a `500` is `runtime.unexpected`;
36
+ * - a `502`, a `503` and a `504` are `runtime.unavailable`, with the `Retry-After` the answer
37
+ * gives;
38
+ * - any other status reads as `readAnswer` reads it: the one core code raised with that status
39
+ * when a client can state it (`request.method_not_allowed` for a `405`), and otherwise
40
+ * `client.response_unreadable` naming the status. A `2xx` the SDK could not read is
41
+ * `client.response_unreadable` too.
42
+ */
43
+ import type { AnswerHeaders, Problem } from "mthds/protocol";
44
+ /** An answer the hosted API, its edge or a proxy in front of it gave. */
45
+ export interface HostedAnswer {
46
+ status: number;
47
+ headers: AnswerHeaders;
48
+ body: string;
49
+ }
50
+ /** An answer read as its problem, and whether that problem is the answer's own body. */
51
+ export interface HostedAnswerReading {
52
+ problem: Problem;
53
+ /** Whether the body is the problem document `problem` was read from, as received. */
54
+ fromBody: boolean;
55
+ }
56
+ /**
57
+ * Read an answer that is not the route's result as its problem, by the rules this module's header
58
+ * gives. `createsRun` is true for a `start` and an `execute`, and only for them.
59
+ */
60
+ export declare function readHostedAnswer(answer: HostedAnswer, options?: {
61
+ createsRun?: boolean;
62
+ }): HostedAnswerReading;
63
+ /** The run a body that is no problem document names as its `pipeline_run_id`, when it names one. */
64
+ export declare function runIdNamedBy(body: string): string | undefined;
65
+ /**
66
+ * `client.outcome_unknown`, the problem of a request that may have started a run whose
67
+ * acknowledgement never reached the caller, naming the run in its parameters and as its member
68
+ * when its id is known.
69
+ */
70
+ export declare function outcomeUnknownProblem(runId?: string): Problem;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * How the SDK reads an answer that is not the route's result as a problem: a refusal, a failure, or
3
+ * a `2xx` it cannot read. The Python twin, `pipelex-sdk`, reads every answer the same way in
4
+ * `pipelex_sdk.answer_reading`, and the shared case file `tests/fixtures/sdk-problems.json` pins the
5
+ * readings both apply.
6
+ *
7
+ * - A body that is a problem document reads as received, every member it carries kept, a
8
+ * `pipelex.*` code and the members the standard does not name included, its `status` taken from
9
+ * the answer when it leaves it out and its `retry_after` from the `Retry-After` header when it
10
+ * gives none and its `retry` is `yes` (`readAnswer` in `mthds/protocol`). On a start or an
11
+ * execute too: the problem's own `retry` says whether a run may exist.
12
+ * - A body that is no problem document is the hosted edge's, a gateway's or a proxy's, which
13
+ * nobody configures, and reads by the rules below. They stay written here until the registry
14
+ * records them and every client generates them.
15
+ *
16
+ * On a request that creates a run, a `start` or an `execute`:
17
+ *
18
+ * - a `2xx` the SDK could not read is `client.outcome_unknown`, since the server accepted the
19
+ * request and only its acknowledgement was lost, naming the run when the body names one;
20
+ * - a `408`, which a proxy may answer for a request it forwarded, and a `5xx` but a `501`, which a
21
+ * gateway answers when the platform was slow or unreachable behind it, are
22
+ * `client.outcome_unknown` too, whatever the time it took: the run may have started behind it.
23
+ *
24
+ * On any route, and on a start for every other status:
25
+ *
26
+ * - a `401` is `auth.missing_credential`: the edge answers it before the platform when the request
27
+ * carries no credential;
28
+ * - a `403` is `auth.invalid_credential`: the edge's authorizer refused the credential, or the
29
+ * route is outside what the credential may call;
30
+ * - a `404` is `request.not_found`: no route matches;
31
+ * - a `408` and a `429` are `runtime.unavailable`, with the `Retry-After` the answer gives: a
32
+ * proxy timed the request out, or the edge throttled it, and a later attempt can pass. The core
33
+ * registry's `account.rate_limited` names the limit and its window, which an edge throttle does
34
+ * not say, so a client cannot state it;
35
+ * - a `500` is `runtime.unexpected`;
36
+ * - a `502`, a `503` and a `504` are `runtime.unavailable`, with the `Retry-After` the answer
37
+ * gives;
38
+ * - any other status reads as `readAnswer` reads it: the one core code raised with that status
39
+ * when a client can state it (`request.method_not_allowed` for a `405`), and otherwise
40
+ * `client.response_unreadable` naming the status. A `2xx` the SDK could not read is
41
+ * `client.response_unreadable` too.
42
+ */
43
+ import { buildProblem, parseJsonBody, readAnswer, readProblemBody, retryAfterSeconds, unreadableAnswer, } from "mthds/protocol";
44
+ /**
45
+ * Read an answer that is not the route's result as its problem, by the rules this module's header
46
+ * gives. `createsRun` is true for a `start` and an `execute`, and only for them.
47
+ */
48
+ export function readHostedAnswer(answer, options = {}) {
49
+ if (readProblemBody(answer.body) !== undefined) {
50
+ return readAnswer(answer, { createsRun: false });
51
+ }
52
+ return { problem: bareAnswerProblem(answer, options.createsRun === true), fromBody: false };
53
+ }
54
+ /** Whether `status` is a success. */
55
+ function isSuccess(status) {
56
+ return status >= 200 && status < 300;
57
+ }
58
+ /** The problem of an answer whose body is no problem document, by the edge's rules. */
59
+ function bareAnswerProblem(answer, createsRun) {
60
+ const { status } = answer;
61
+ if (createsRun && (isSuccess(status) || status === 408 || (status >= 500 && status !== 501))) {
62
+ return outcomeUnknownProblem(runIdNamedBy(answer.body));
63
+ }
64
+ if (isSuccess(status))
65
+ return unreadableAnswer(status);
66
+ switch (status) {
67
+ case 401:
68
+ return buildProblem("auth.missing_credential", {}, { status });
69
+ case 403:
70
+ return buildProblem("auth.invalid_credential", {}, { status });
71
+ case 404:
72
+ return buildProblem("request.not_found", {}, { status });
73
+ case 500:
74
+ return buildProblem("runtime.unexpected", {}, { status });
75
+ case 408:
76
+ case 429:
77
+ case 502:
78
+ case 503:
79
+ case 504:
80
+ return buildProblem("runtime.unavailable", {}, { status, retryAfter: retryAfterSeconds(answer.headers) });
81
+ default:
82
+ return readAnswer(answer, { createsRun: false }).problem;
83
+ }
84
+ }
85
+ /** The run a body that is no problem document names as its `pipeline_run_id`, when it names one. */
86
+ export function runIdNamedBy(body) {
87
+ const reading = parseJsonBody(body);
88
+ if (!reading.ok)
89
+ return undefined;
90
+ const { value } = reading;
91
+ if (typeof value !== "object" || value === null || Array.isArray(value))
92
+ return undefined;
93
+ const runId = value.pipeline_run_id;
94
+ return typeof runId === "string" && runId !== "" ? runId : undefined;
95
+ }
96
+ /**
97
+ * `client.outcome_unknown`, the problem of a request that may have started a run whose
98
+ * acknowledgement never reached the caller, naming the run in its parameters and as its member
99
+ * when its id is known.
100
+ */
101
+ export function outcomeUnknownProblem(runId) {
102
+ if (runId === undefined)
103
+ return buildProblem("client.outcome_unknown", {});
104
+ return buildProblem("client.outcome_unknown", { pipeline_run_id: runId }, { pipeline_run_id: runId });
105
+ }
106
+ //# sourceMappingURL=answer-reading.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"answer-reading.js","sourceRoot":"","sources":["../src/answer-reading.ts"],"names":[],"mappings":"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"}
package/dist/artifacts.js CHANGED
@@ -27,23 +27,11 @@
27
27
  * only `downloadArtifacts` touches a filesystem.
28
28
  */
29
29
  import { ApiResponseError, ArtifactAuthenticationError, ArtifactFetchError, ArtifactOperationError, RunFailedError, RunStillRunningError, ScopeUnavailableError, } from "./errors.js";
30
+ import { argumentInvalidProblem, responseUnreadableProblem } from "./errors.js";
31
+ import { buildProblem } from "mthds/protocol";
30
32
  import { absenceCause, loadNodeHost } from "./node-host.js";
31
33
  import { MAX_TIMER_DELAY_MS, isTimerDelay } from "./timers.js";
32
34
  // ── Constants ────────────────────────────────────────────────────────
33
- /**
34
- * The verdict of an argument the artifact operations refuse (a `scope`, a bound, a location):
35
- * the caller's to fix, and asking again unchanged cannot pass.
36
- */
37
- const ARGUMENT_REFUSED = {
38
- verdict: { errorDomain: "input", retryable: false },
39
- };
40
- /**
41
- * The verdict of an environment that cannot hold a download (a runtime with no filesystem, a
42
- * directory that cannot be created): someone changes the environment first.
43
- */
44
- const ENVIRONMENT_REFUSED = {
45
- verdict: { errorDomain: "config", retryable: false },
46
- };
47
35
  /** The scheme of a durable storage reference. */
48
36
  export const PIPELEX_STORAGE_SCHEME = "pipelex-storage://";
49
37
  /**
@@ -283,14 +271,14 @@ export function artifactFilename(location, contentType, scope) {
283
271
  const loose = location;
284
272
  const uri = loose?.uri;
285
273
  if (typeof uri !== "string") {
286
- throw new ArtifactOperationError(`artifactFilename needs a location carrying its reference as a string "uri"; got ${String(uri)}.`, ARGUMENT_REFUSED);
274
+ throw new ArtifactOperationError(`artifactFilename needs a location carrying its reference as a string "uri"; got ${String(uri)}.`, { problem: argumentInvalidProblem("location", "not_a_location") });
287
275
  }
288
276
  const foundAt = loose?.found_at;
289
277
  const first = Array.isArray(foundAt) ? foundAt[0] : undefined;
290
278
  const segments = typeof first === "string" ? parsePath(first) : undefined;
291
279
  if (segments === undefined) {
292
280
  throw new ArtifactOperationError(`artifactFilename needs a location whose first "found_at" entry is a path such as ` +
293
- `"$.items[0].url"; got ${typeof first === "string" ? JSON.stringify(first) : String(first)}.`, ARGUMENT_REFUSED);
281
+ `"$.items[0].url"; got ${typeof first === "string" ? JSON.stringify(first) : String(first)}.`, { problem: argumentInvalidProblem("location", "not_a_path") });
294
282
  }
295
283
  return filenameFor(segments, uri, contentType, scope);
296
284
  }
@@ -360,7 +348,7 @@ function extensionOf(name) {
360
348
  }
361
349
  function requireScope(scope) {
362
350
  if (scope !== "main_stuff" && scope !== "working_memory") {
363
- throw new ArtifactOperationError(`"scope" must be "main_stuff" or "working_memory", got ${String(scope)}.`, ARGUMENT_REFUSED);
351
+ throw new ArtifactOperationError(`"scope" must be "main_stuff" or "working_memory", got ${String(scope)}.`, { problem: argumentInvalidProblem("scope", "unknown_value") });
364
352
  }
365
353
  }
366
354
  // ── resolveArtifacts ─────────────────────────────────────────────────
@@ -384,7 +372,7 @@ export async function resolveArtifacts(client, uris, options = {}) {
384
372
  if (!Array.isArray(resolved) || resolved.length !== chunk.length) {
385
373
  throw new ArtifactOperationError(`The bulk resolve route answered ${Array.isArray(resolved) ? resolved.length : "no"} ` +
386
374
  `item(s) for ${chunk.length} reference(s) — a malformed answer, so no reference can be ` +
387
- "matched to its verdict.");
375
+ "matched to its verdict.", { problem: responseUnreadableProblem(200) });
388
376
  }
389
377
  items.push(...resolved);
390
378
  }
@@ -430,13 +418,15 @@ function fetchBounds(options) {
430
418
  // A longer delay overflows the timer, which then fires at once as a false timeout.
431
419
  if (!isTimerDelay(timeoutMs)) {
432
420
  throw new ArtifactOperationError(`"timeoutMs" must be a positive number no larger than ${MAX_TIMER_DELAY_MS}, got ` +
433
- `${String(timeoutMs)}.`, ARGUMENT_REFUSED);
421
+ `${String(timeoutMs)}.`, { problem: argumentInvalidProblem("timeoutMs", "not_positive") });
434
422
  }
435
423
  return { maxBytes, timeoutMs, allowHttp: options.allowHttp ?? false, signal: options.signal };
436
424
  }
437
425
  function requirePositive(name, value) {
438
426
  if (!Number.isFinite(value) || value <= 0) {
439
- throw new ArtifactOperationError(`"${name}" must be a positive number, got ${String(value)}.`, ARGUMENT_REFUSED);
427
+ throw new ArtifactOperationError(`"${name}" must be a positive number, got ${String(value)}.`, {
428
+ problem: argumentInvalidProblem(name, "not_positive"),
429
+ });
440
430
  }
441
431
  }
442
432
  /**
@@ -529,7 +519,10 @@ async function fetchResolvedUrl(uri, downloadUrl, bounds, dispatcher, release) {
529
519
  }
530
520
  const failure = (err) => {
531
521
  if (timedOut) {
532
- return new ArtifactFetchError(`Fetching the artifact timed out after ${bounds.timeoutMs}ms.`, uri, "timeout", undefined, { cause: err });
522
+ return new ArtifactFetchError(`Fetching the artifact timed out after ${bounds.timeoutMs}ms.`, uri, "timeout", undefined, {
523
+ cause: err,
524
+ problem: buildProblem("client.timeout", { seconds: bounds.timeoutMs / 1000 }),
525
+ });
533
526
  }
534
527
  // The caller's reason, not `err`: a browser rejects the fetch, and errors a body
535
528
  // cut short, with a generic AbortError rather than the signal's reason.
@@ -537,7 +530,7 @@ async function fetchResolvedUrl(uri, downloadUrl, bounds, dispatcher, release) {
537
530
  return userSignal.reason;
538
531
  if (err instanceof ArtifactFetchError)
539
532
  return err;
540
- return new ArtifactFetchError(`The artifact could not be fetched: ${err instanceof Error ? err.message : String(err)}.`, uri, "network", undefined, { cause: err });
533
+ return new ArtifactFetchError(`The artifact could not be fetched: ${err instanceof Error ? err.message : String(err)}.`, uri, "network", undefined, { cause: err, problem: buildProblem("client.unreachable", { base_url: url.origin }) });
541
534
  };
542
535
  if (controller.signal.aborted) {
543
536
  // Already aborted before the request: the same outcome `fetch` would give,
@@ -570,7 +563,7 @@ async function fetchResolvedUrl(uri, downloadUrl, bounds, dispatcher, release) {
570
563
  if (Number.isFinite(declared) && declared > bounds.maxBytes) {
571
564
  await discard(response);
572
565
  releaseOnce();
573
- throw new ArtifactFetchError(`The artifact is ${formatMiB(declared)}, over the ${formatMiB(bounds.maxBytes)} cap.`, uri, "too_large", response.status);
566
+ throw new ArtifactFetchError(`The artifact is ${formatMiB(declared)}, over the ${formatMiB(bounds.maxBytes)} cap.`, uri, "too_large", response.status, { problem: downloadLimitProblem(bounds.maxBytes) });
574
567
  }
575
568
  if (response.body === null) {
576
569
  releaseOnce();
@@ -612,7 +605,7 @@ async function fetchResolvedUrl(uri, downloadUrl, bounds, dispatcher, release) {
612
605
  if (total > bounds.maxBytes) {
613
606
  await upstream.cancel().catch(() => undefined);
614
607
  releaseOnce();
615
- streamController.error(new ArtifactFetchError(`The artifact crossed the ${formatMiB(bounds.maxBytes)} cap mid-stream.`, uri, "too_large", response.status));
608
+ streamController.error(new ArtifactFetchError(`The artifact crossed the ${formatMiB(bounds.maxBytes)} cap mid-stream.`, uri, "too_large", response.status, { problem: downloadLimitProblem(bounds.maxBytes) }));
616
609
  return;
617
610
  }
618
611
  streamController.enqueue(chunk.value);
@@ -654,6 +647,10 @@ function decodedHeaders(headers) {
654
647
  decoded.delete("content-length");
655
648
  return decoded;
656
649
  }
650
+ /** `client.download_limit_exceeded`, naming the cap in bytes. */
651
+ function downloadLimitProblem(maxBytes) {
652
+ return buildProblem("client.download_limit_exceeded", { limit: maxBytes });
653
+ }
657
654
  function statusRefusal(uri, response) {
658
655
  const status = response.status;
659
656
  // A browser hands a manual redirect back as an opaque-redirect response, whose
@@ -769,7 +766,7 @@ async function readCompletedResults(client, runId, scope, signal) {
769
766
  (state.retry_after_seconds != null ? ` — retry in ${state.retry_after_seconds}s.` : "."), runId, state.retry_after_seconds);
770
767
  }
771
768
  if (state.state === "failed") {
772
- throw new RunFailedError(state.message, runId, state.status, { error: state.error });
769
+ throw new RunFailedError(state.message, runId, state.status, { problem: state.problem });
773
770
  }
774
771
  return state.result;
775
772
  }
@@ -801,7 +798,10 @@ export async function downloadArtifacts(client, request) {
801
798
  throw new ArtifactOperationError(loaded.reason === null
802
799
  ? "downloadArtifacts writes to a filesystem and is Node-only. In another runtime, resolve " +
803
800
  "with resolveArtifacts or stream with fetchArtifact instead."
804
- : `downloadArtifacts cannot write to the filesystem: ${loaded.reason}`, { ...ENVIRONMENT_REFUSED, ...absenceCause(loaded) });
801
+ : `downloadArtifacts cannot write to the filesystem: ${loaded.reason}`, {
802
+ ...absenceCause(loaded),
803
+ problem: buildProblem("runtime.unsupported", { capability: "writing to the filesystem" }),
804
+ });
805
805
  }
806
806
  const host = loaded.host;
807
807
  const scope = request.scope ?? "main_stuff";
@@ -809,7 +809,7 @@ export async function downloadArtifacts(client, request) {
809
809
  const concurrency = request.concurrency ?? DEFAULT_DOWNLOAD_CONCURRENCY;
810
810
  const maxTotalBytes = request.maxTotalBytes ?? DEFAULT_DOWNLOAD_MAX_TOTAL_BYTES;
811
811
  if (!Number.isInteger(concurrency) || concurrency < 1) {
812
- throw new ArtifactOperationError(`"concurrency" must be a positive integer, got ${String(concurrency)}.`, ARGUMENT_REFUSED);
812
+ throw new ArtifactOperationError(`"concurrency" must be a positive integer, got ${String(concurrency)}.`, { problem: argumentInvalidProblem("concurrency", "not_positive") });
813
813
  }
814
814
  requirePositive("maxTotalBytes", maxTotalBytes);
815
815
  const bounds = fetchBounds(request);
@@ -817,7 +817,7 @@ export async function downloadArtifacts(client, request) {
817
817
  const hasResults = request.results != null;
818
818
  if (hasRunId === hasResults) {
819
819
  throw new ArtifactOperationError("downloadArtifacts takes exactly one of `run_id` (the results are re-read) or `results` " +
820
- "(a RunResults in hand).", ARGUMENT_REFUSED);
820
+ "(a RunResults in hand).", { problem: argumentInvalidProblem("run_id", "not_exactly_one_selector") });
821
821
  }
822
822
  const results = hasResults
823
823
  ? request.results
@@ -845,7 +845,10 @@ export async function downloadArtifacts(client, request) {
845
845
  await fs.mkdir(dir, { recursive: true });
846
846
  }
847
847
  catch (cause) {
848
- throw new ArtifactOperationError(`The download directory "${dir}" cannot be created or used: ${cause instanceof Error ? cause.message : String(cause)}.`, { cause, ...ENVIRONMENT_REFUSED });
848
+ throw new ArtifactOperationError(`The download directory "${dir}" cannot be created or used: ${cause instanceof Error ? cause.message : String(cause)}.`, {
849
+ cause,
850
+ problem: buildProblem("client.write_failed", { path: dir, reason: osErrorReason(cause) }),
851
+ });
849
852
  }
850
853
  // Dense, never sparse: `map` skips the holes of `new Array(n)`, and the verdict
851
854
  // must name every reference, reached or not.
@@ -1061,15 +1064,25 @@ function assembleVerdict(scope, artifacts, aborted) {
1061
1064
  verdict.aborted = true;
1062
1065
  return verdict;
1063
1066
  }
1064
- /** An item's `[code, detail]` for an error thrown while fetching or reading it. */
1067
+ /**
1068
+ * An item's `[code, detail]` for an error thrown while fetching or reading it: the fetch's own
1069
+ * reason for an `ArtifactFetchError`, `aborted` for the caller's abort, and `unknown` for any other
1070
+ * error, which is no network failure but a fault the SDK cannot place, such as a defect of its own
1071
+ * or of the caller's code, so it is never read as one a retry can pass.
1072
+ */
1065
1073
  function classifyFailure(err, signal) {
1066
1074
  if (err instanceof ArtifactFetchError)
1067
- return [err.code, err.message];
1075
+ return [err.reason, err.message];
1068
1076
  if (signal?.aborted === true)
1069
1077
  return ["aborted", "The download was aborted while this artifact was being fetched."];
1070
1078
  return [
1071
- "network",
1079
+ "unknown",
1072
1080
  `The artifact could not be read: ${err instanceof Error ? err.message : String(err)}.`,
1073
1081
  ];
1074
1082
  }
1083
+ /** Why a directory could not be made, as the operating system's code (`EACCES`), or `unwritable`. */
1084
+ function osErrorReason(cause) {
1085
+ const code = cause?.code;
1086
+ return typeof code === "string" && /^[A-Z][A-Z0-9_]*$/.test(code) ? code : "unwritable";
1087
+ }
1075
1088
  //# sourceMappingURL=artifacts.js.map