@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/dist/errors.js CHANGED
@@ -1,99 +1,112 @@
1
1
  /**
2
2
  * Pipelex SDK errors — the argument, transport, run-lifecycle, input-preparation and artifact
3
- * errors `PipelexApiClient` raises. Every one derives from `PipelexRequestError`, itself a
4
- * `PipelineRequestError` (the protocol base, re-exported from `mthds/protocol`), and so carries
5
- * a verdict: `retryable` and `errorDomain`, always decided, and `runMayHaveStarted`, which says
6
- * whether a failed start or execute may still have started a run. The one exported error outside
7
- * the family, `CodegenLockError`, declares the verdict's two members itself.
8
- */
9
- import { PipelineRequestError, buildProblem, isProblem, parseJsonBody, readAnswer, unreadableAnswer, } from "mthds/protocol";
10
- import { isPlainObject } from "./error-models.js";
11
- import { fallbackVerdict, problemVerdict } from "./error-verdicts.js";
12
- import { answerMayHaveStartedRun, transportMayHaveStartedRun } from "./run-may-have-started.js";
13
- export { PipelineRequestError };
14
- const ERROR_DOMAINS = new Set(["input", "config", "runtime"]);
15
- /** Whether a value is one of the three error domains. */
16
- export function isErrorDomain(value) {
17
- return typeof value === "string" && ERROR_DOMAINS.has(value);
18
- }
19
- /** Build a verdict; each class below declares its own with it. */
20
- function makeVerdict(errorDomain, retryable) {
21
- return Object.freeze({ errorDomain, retryable });
22
- }
23
- /**
24
- * The base of every error this SDK raises over a request, carrying the verdict as two readonly
25
- * own properties, `retryable` and `errorDomain`, and a third, `runMayHaveStarted`. Each subclass
26
- * passes its verdict to this constructor, so a class that forgets one does not compile, and every
27
- * class says its verdict where it is defined.
28
- *
29
- * `runMayHaveStarted` is false unless the subclass passes it: only `ApiUnreachableError` and
30
- * `ApiResponseError` built for a request that may create a run, and `PipelineExecuteTimeoutError`,
31
- * ever carry it as true, and when they do, `retryable` is false whatever the verdict passed.
32
- *
33
- * It refines the standard's `PipelineRequestError`, so `instanceof PipelineRequestError` still
34
- * matches every SDK error. To read a verdict from anything a `catch` holds, use
35
- * `errorVerdictOf`, which also reads it off an error raised by another copy of this SDK, and
36
- * `runMayHaveStartedOf` for the third member.
37
- */
38
- export class PipelexRequestError extends PipelineRequestError {
39
- /** Whether asking again can plausibly succeed. Always decided. */
40
- retryable;
41
- /** Who can fix the failure: `input`, `config` or `runtime`. Always decided. */
42
- errorDomain;
43
- /**
44
- * Whether the failed request may still have started a run whose id never reached the caller:
45
- * a `start` or an `execute` (and so `startAndWaitForResult`'s start, or the blocking execute it
46
- * falls back to) that failed in a way that does not prove the server started nothing. When it
47
- * is true, `retryable` is false: look the run up in the run history before starting it again.
48
- * False for every other request, a wait on a run already named included. Always decided.
49
- */
50
- runMayHaveStarted;
51
- constructor(message, verdict, options) {
52
- super(message, options);
53
- this.name = "PipelexRequestError";
54
- this.runMayHaveStarted = options?.runMayHaveStarted === true;
55
- // Asking again could start a second run, so a run that may have started is never retryable.
56
- this.retryable = this.runMayHaveStarted ? false : verdict.retryable;
57
- this.errorDomain = verdict.errorDomain;
58
- }
3
+ * errors `PipelexApiClient` raises. Every one derives from `PipelexRequestError`, itself the MTHDS
4
+ * standard's `ProblemError` (re-exported from `mthds/protocol`), and so carries its failure as a
5
+ * problem document: the runtime's own when it answered with one, and otherwise the problem the SDK
6
+ * builds for a failure it detects itself, under a registered code. A program branches on `code`,
7
+ * and on `domain`, `retry` and `nextStep` when it handles several codes alike; the rest of the
8
+ * problem (`params`, `location`, `errors`, `pipeline_run_id`, `request_id` and any member the
9
+ * runtime added) is read off `problem`. The Python twin, `pipelex-sdk`, builds the same code for
10
+ * every failure, which the shared case file `tests/fixtures/sdk-problems.json` pins in both suites.
11
+ *
12
+ * The exported errors outside the family, `CodegenLockError` and `ExactJsonUnsupportedError`, are
13
+ * `ProblemError`s too.
14
+ */
15
+ import { ProblemError, buildProblem, isProblem, readAnswer, readProblemBody } from "mthds/protocol";
16
+ import { outcomeUnknownProblem, readHostedAnswer } from "./answer-reading.js";
17
+ import { requestMayHaveLeft } from "./transport-reading.js";
18
+ export { ProblemError };
19
+ /** The `cause` option of an error, given only when there is one, so `cause` stays absent otherwise. */
20
+ function causeOption(cause) {
21
+ return cause === undefined ? undefined : { cause };
22
+ }
23
+ // ── The problems this module builds ──────────────────────────────────
24
+ /**
25
+ * The problem of an argument refused before any request left: `client.argument_invalid`, naming
26
+ * the argument and why it was refused, as an identifier (`not_a_string`, `mutually_exclusive`, …).
27
+ */
28
+ export function argumentInvalidProblem(argument, reason) {
29
+ return buildProblem("client.argument_invalid", { argument, reason });
59
30
  }
60
31
  /**
61
- * The verdict of anything a `catch` holds: the pair from an `Error` carrying a boolean
62
- * `retryable` and a known `errorDomain`, else the verdict of the MTHDS standard's problem an
63
- * `Error` carries as `problem`, and `undefined` otherwise.
64
- *
65
- * The check is structural rather than `instanceof`, so it reads every error of this SDK, of
66
- * another copy of it installed beside this one, and of a consumer's own subclass. An error of the
67
- * standard's own client (`mthds`) carries its verdict on its problem, as `domain` and `retry` in
68
- * the standard's vocabulary, and reads as that problem's verdict in this SDK's
69
- * (`problemVerdict`), whatever the runner's body held beside it. `undefined` means the error
70
- * carries no verdict: the `RangeError` or `TypeError` the SDK raises for an argument of the
71
- * wrong type or range before any request is sent (a bug in the calling code), the caller's own
72
- * abort, which the client rethrows untouched, or anything else a `try` block threw. An argument
73
- * the client refuses for what it asks is a `RequestArgumentError`, which carries one.
74
- */
75
- export function errorVerdictOf(err) {
76
- if (!(err instanceof Error))
77
- return undefined;
78
- const { retryable, errorDomain, problem } = err;
79
- if (typeof retryable === "boolean" && isErrorDomain(errorDomain)) {
80
- return { retryable, errorDomain };
32
+ * The problem of an answer the SDK cannot use as the route's answer: `client.response_unreadable`,
33
+ * naming its status, and the run it is about as its member when there is one.
34
+ */
35
+ export function responseUnreadableProblem(status, runId) {
36
+ return buildProblem("client.response_unreadable", { status }, runId === undefined ? {} : { pipeline_run_id: runId });
37
+ }
38
+ /** The problem the standard reads from an answer's status alone, as for a body that is no problem document. */
39
+ export function statusProblem(status) {
40
+ return readAnswer({ status, headers: {}, body: "" }, { createsRun: false }).problem;
41
+ }
42
+ /**
43
+ * Whether a storage answer's `status` refuses the request for its timing or fails in a way a later
44
+ * attempt can pass: a `408`, a `429`, or a `5xx` other than `501`.
45
+ */
46
+ export function isTransientStorageStatus(status) {
47
+ return status === 408 || status === 429 || (status >= 500 && status <= 599 && status !== 501);
48
+ }
49
+ /**
50
+ * The problem of a storage answer the SDK reads by its status, storage answering no problem
51
+ * document: `runtime.unavailable` for a transient status ({@link isTransientStorageStatus}), so
52
+ * that asking again can succeed; `runtime.unsupported` for a `501`, storage not implementing the
53
+ * request it was sent; and `client.response_unreadable` naming the status otherwise, an answer the
54
+ * SDK has no reading for.
55
+ */
56
+ export function storageStatusProblem(status) {
57
+ if (isTransientStorageStatus(status))
58
+ return buildProblem("runtime.unavailable", {});
59
+ if (status === 501)
60
+ return buildProblem("runtime.unsupported", { capability: "this storage request" });
61
+ return responseUnreadableProblem(status);
62
+ }
63
+ /** The problem of a request refused for its credential: `auth.invalid_credential`, or `auth.missing_credential` when it carried none. */
64
+ function credentialRefusedProblem(credentialSent) {
65
+ return credentialSent
66
+ ? buildProblem("auth.invalid_credential", {})
67
+ : buildProblem("auth.missing_credential", {});
68
+ }
69
+ /** The problem an error wrapping another carries: the wrapped error's, when it carries one. */
70
+ function wrappedProblem(cause) {
71
+ return cause instanceof ProblemError ? cause.problem : undefined;
72
+ }
73
+ /**
74
+ * The problem of a refused credential, in this order: the server's own, when the wrapped
75
+ * `ApiResponseError` answered a problem document; then what the caller knows of its own request,
76
+ * when it says whether the request carried a credential, which the edge's bare `401` or `403` does
77
+ * not say; then the wrapped error's problem, its reading of that status; and otherwise
78
+ * `auth.invalid_credential`.
79
+ */
80
+ function credentialProblem(cause, credentialSent) {
81
+ if (cause instanceof ApiResponseError && readProblemBody(cause.responseBody) !== undefined) {
82
+ return cause.problem;
81
83
  }
82
- return isProblem(problem) ? problemVerdict(problem) : undefined;
84
+ if (credentialSent !== undefined)
85
+ return credentialRefusedProblem(credentialSent);
86
+ return wrappedProblem(cause) ?? credentialRefusedProblem(true);
83
87
  }
88
+ // ── The base every request error derives from ────────────────────────
84
89
  /**
85
- * Whether anything a `catch` holds is a failed run start that may still have started a run: true
86
- * only for an `Error` carrying `runMayHaveStarted` as `true`.
87
- *
88
- * The check is structural, as `errorVerdictOf`'s is, so it reads an error raised by another copy
89
- * of this SDK installed beside this one. Anything else is false: an error of a request that
90
- * cannot start a run, an `mthds` error, which the standard's client never raises over a start of
91
- * this SDK, and a value that is not an `Error`.
90
+ * The base of every error this SDK raises over a request, carrying the failure as `problem`.
91
+ *
92
+ * It refines the standard's `ProblemError`, so every getter that reads the problem is there:
93
+ * `code`, the member a program branches on; `domain`, who must act (`input`, `method`,
94
+ * `account`, `operator` or `runtime`); `retry`, whether sending the same request again can succeed
95
+ * (`yes`, `no`, or `outcome_unknown` where a run may already exist and must be read, never started
96
+ * again); `nextStep`, what to do; `retryAfter`, the seconds to wait before a repeat when the
97
+ * problem gives them; and `requestId`, the id to hand to support. `instanceof ProblemError` and
98
+ * `instanceof PipelineRequestError` match every SDK error, as does `mthds`'s `isProblemError`.
99
+ *
100
+ * The message is the SDK's own sentence, as it always was, rather than the problem's: the problem's
101
+ * `detail` and `next_step.detail` are on `problem` for whoever renders them. Its subclasses are
102
+ * meant to be raised, never this class itself.
92
103
  */
93
- export function runMayHaveStartedOf(err) {
94
- if (!(err instanceof Error))
95
- return false;
96
- return err.runMayHaveStarted === true;
104
+ export class PipelexRequestError extends ProblemError {
105
+ constructor(message, problem, options) {
106
+ super(problem, options);
107
+ this.name = "PipelexRequestError";
108
+ this.message = message;
109
+ }
97
110
  }
98
111
  // ── Argument refusals ────────────────────────────────────────────────
99
112
  /**
@@ -107,15 +120,16 @@ export function runMayHaveStartedOf(err) {
107
120
  * to 255 visible ASCII characters, or a base URL that is not host-only. Nothing reached the API,
108
121
  * so the message says what to change.
109
122
  *
110
- * Its verdict is `input`, not retryable — the caller must change the arguments — unless
111
- * `options.verdict` declares another: the client declares `config` for a base URL that is not
112
- * host-only, since it typically comes from `PIPELEX_BASE_URL`. A refusal the standard's own
113
- * check raises (`assertExclusiveRunSources`) is rethrown as this class with the same message, the
114
- * standard's error as `cause`.
123
+ * Its problem is `client.argument_invalid`, unless `options.problem` gives another: the client
124
+ * gives `client.not_configured` naming `base_url` for a base URL that is not host-only, since it
125
+ * typically comes from `PIPELEX_BASE_URL`. One built from a message alone names the argument
126
+ * `arguments`, for the reason `unspecified`. A refusal the standard's own check raises
127
+ * (`assertExclusiveRunSources`) is rethrown as this class with the same message, the standard's
128
+ * error as `cause`.
115
129
  */
116
130
  export class RequestArgumentError extends PipelexRequestError {
117
131
  constructor(message, options) {
118
- super(message, options?.verdict ?? makeVerdict("input", false), options);
132
+ super(message, options?.problem ?? argumentInvalidProblem("arguments", "unspecified"), causeOption(options?.cause));
119
133
  this.name = "RequestArgumentError";
120
134
  }
121
135
  }
@@ -126,25 +140,34 @@ export class RequestArgumentError extends PipelexRequestError {
126
140
  * subclass to branch on the semantic category. All preparation failures are
127
141
  * raised BEFORE any run is created — a run never triggers a hidden upload.
128
142
  *
129
- * Its verdict is `input`, not retryable — the asset, the inputs or the method selector must
130
- * change — unless `options.verdict` declares another: the SDK's own subclasses do, and so can a
131
- * consumer's, such as a form reporting a missing input in its own words.
143
+ * Its problem is the one `options.problem` gives: the SDK's own subclasses state theirs, a direct
144
+ * raise gives `client.argument_invalid` for an argument refused or the wrapped error's for a
145
+ * refusal the API answered, and a consumer's subclass, such as a form reporting a missing input in
146
+ * its own words, may give any. One built with none, from a message alone, carries
147
+ * `client.argument_invalid` naming `inputs`, for the reason `unspecified`.
132
148
  */
133
149
  export class InputPreparationError extends PipelexRequestError {
134
150
  constructor(message, options) {
135
- super(message, options?.verdict ?? makeVerdict("input", false), options);
151
+ super(message, options?.problem ?? argumentInvalidProblem("inputs", "unspecified"), causeOption(options?.cause));
136
152
  this.name = "InputPreparationError";
137
153
  }
138
154
  }
139
155
  /**
140
156
  * A local asset could not be turned into bytes: a missing or unreadable path, or
141
157
  * a path string in a non-Node runtime (path strings are Node-only). `source` is
142
- * the offending path.
158
+ * the offending path. Its problem is `input.file_unreadable`, naming the path.
143
159
  */
144
160
  export class InvalidLocalSourceError extends InputPreparationError {
145
161
  source;
146
162
  constructor(message, source, options) {
147
- super(message, options);
163
+ super(message, {
164
+ ...options,
165
+ problem: buildProblem("input.file_unreadable", {
166
+ input: source,
167
+ file_type: "file",
168
+ reason: "unreadable",
169
+ }),
170
+ });
148
171
  this.name = "InvalidLocalSourceError";
149
172
  this.source = source;
150
173
  }
@@ -160,13 +183,20 @@ export class InvalidLocalSourceError extends InputPreparationError {
160
183
  * As on `UploadNotAllowedError`, `inputName` is the input the value belongs to and `inputPath`
161
184
  * where it sits inside that input (`report.attachments[1]`, or the name alone for a top-level
162
185
  * file). The message names that path and describes the value by its kind, never by its content,
163
- * since the value is the caller's data.
186
+ * since the value is the caller's data. Its problem is `input.invalid`, naming the input and the
187
+ * path inside it.
164
188
  */
165
189
  export class InvalidInputValueError extends InputPreparationError {
166
190
  inputName;
167
191
  inputPath;
168
192
  constructor(message, details, options) {
169
- super(message, options);
193
+ super(message, {
194
+ ...options,
195
+ problem: buildProblem("input.invalid", {
196
+ input: details.inputName,
197
+ field: details.inputPath,
198
+ }),
199
+ });
170
200
  this.name = "InvalidInputValueError";
171
201
  this.inputName = details.inputName;
172
202
  this.inputPath = details.inputPath;
@@ -184,8 +214,9 @@ const UPLOAD_NOT_ALLOWED_KIND_WORDS = {
184
214
  * reads a file of its own because the caller named it. `inputName` is the input the value belongs
185
215
  * to, `inputPath` where it sits inside that input (`report.attachments[1]`, or the name alone for a
186
216
  * top-level file), and `valueKind` what it is. The value itself is neither in the message nor on
187
- * the error, since it is the caller's data. Its verdict is `input`, not retryable: the caller must
188
- * pass an `http(s)` URL or a `pipelex-storage://` reference instead.
217
+ * the error, since it is the caller's data. Its problem is `client.argument_invalid`, naming the
218
+ * input's path and the value's kind as the reason: the caller must pass an `http(s)` URL or a
219
+ * `pipelex-storage://` reference instead.
189
220
  */
190
221
  export class UploadNotAllowedError extends InputPreparationError {
191
222
  inputName;
@@ -194,7 +225,7 @@ export class UploadNotAllowedError extends InputPreparationError {
194
225
  constructor(details, options) {
195
226
  super(`Input "${details.inputPath}" is ${UPLOAD_NOT_ALLOWED_KIND_WORDS[details.valueKind]}, which ` +
196
227
  "would need an upload, and this preparation uploads nothing: pass an http(s) URL or a " +
197
- "pipelex-storage:// reference instead.", options);
228
+ "pipelex-storage:// reference instead.", { ...options, problem: argumentInvalidProblem(details.inputPath, details.valueKind) });
198
229
  this.name = "UploadNotAllowedError";
199
230
  this.inputName = details.inputName;
200
231
  this.inputPath = details.inputPath;
@@ -203,18 +234,19 @@ export class UploadNotAllowedError extends InputPreparationError {
203
234
  }
204
235
  /**
205
236
  * The pipe I/O answer said the method does not load (`is_valid: false`), so its signature cannot
206
- * be read and no input can be prepared: the method, not the inputs, must change.
207
- * `validationErrors` holds the answer's items, each kept when it carries a string `category` and
208
- * `message`, possibly none; `serverMessage` is the answer's own `message` when it is a string. The
209
- * error's message names the first item's message, else the answer's own.
237
+ * be read and no input can be prepared: the method, not the inputs, must change. Its problem is
238
+ * `method.invalid`, whose `errors` are the answer's problem items, each read member by member
239
+ * (see `readProblemItems`), possibly none; `serverMessage` is the answer's own `message` when it is
240
+ * a string. The error's message names the first item's `detail`, else the answer's own message.
210
241
  */
211
242
  export class MethodLoadError extends InputPreparationError {
212
- validationErrors;
213
243
  serverMessage;
214
244
  constructor(message, details, options) {
215
- super(message, options);
245
+ super(message, {
246
+ ...options,
247
+ problem: buildProblem("method.invalid", {}, { errors: details.errors }),
248
+ });
216
249
  this.name = "MethodLoadError";
217
- this.validationErrors = details.validationErrors;
218
250
  this.serverMessage = details.serverMessage;
219
251
  }
220
252
  }
@@ -225,48 +257,63 @@ export class MethodLoadError extends InputPreparationError {
225
257
  * time out or throttle the request, a `400 RequestTimeout` and a `409
226
258
  * ConditionalRequestConflict` are an `UploadTransportError`. The SDK
227
259
  * does not impose a client-side cap; it surfaces the refusal. `filename` and
228
- * `status` locate it, and `code` says why: the SDK sets it on every one it raises,
260
+ * `status` locate it, and `reason` says why: the SDK sets it on every one it raises,
229
261
  * so it is undefined only on one a caller constructs without it.
262
+ *
263
+ * Its problem is the refusal's as the API answered it, which `uploadFile` passes, and otherwise
264
+ * the one the standard reads from `status` alone, storage answering no problem document.
230
265
  */
231
266
  export class RejectedAssetError extends InputPreparationError {
232
267
  filename;
233
268
  status;
234
- code;
269
+ reason;
235
270
  constructor(message, filename, status, options) {
236
- super(message, options);
271
+ super(message, {
272
+ cause: options?.cause,
273
+ problem: options?.problem ?? statusProblem(status),
274
+ });
237
275
  this.name = "RejectedAssetError";
238
276
  this.filename = filename;
239
277
  this.status = status;
240
- this.code = options?.code;
278
+ this.reason = options?.reason;
241
279
  }
242
280
  }
243
281
  /**
244
282
  * The configured deployment does not support upload (no `/v1/upload` route, seen
245
283
  * as a `404` other than a test file's unknown method, which is a {@link TestFileRefusedError}).
246
284
  * Upload is a hosted Pipelex-product capability even though the SDK
247
- * can be pointed at other base URLs. Its verdict is `config`, not retryable: the base URL
248
- * must point at a deployment that serves upload. `filename` is the file whose upload met it, as
249
- * `uploadFile` sent it.
285
+ * can be pointed at other base URLs. Its problem is `runtime.unsupported`, naming file upload as
286
+ * the capability the deployment lacks: the base URL must point at a deployment that serves
287
+ * upload. `filename` is the file whose upload met it, as `uploadFile` sent it.
250
288
  */
251
289
  export class UnsupportedUploadCapabilityError extends InputPreparationError {
252
290
  filename;
253
291
  constructor(message, options) {
254
- super(message, { ...options, verdict: makeVerdict("config", false) });
292
+ super(message, {
293
+ cause: options?.cause,
294
+ problem: buildProblem("runtime.unsupported", { capability: "file upload" }),
295
+ });
255
296
  this.name = "UnsupportedUploadCapabilityError";
256
297
  this.filename = options?.filename;
257
298
  }
258
299
  }
259
300
  /**
260
301
  * Upload was not authorized — a `401`, or a `403` from the upload route other than a test file's
261
- * `role_not_permitted`, which is a {@link TestFileRefusedError}. Its verdict is `config`, not
262
- * retryable: the credential must change. `filename` is the file whose upload was refused, as
302
+ * refused role, which is a {@link TestFileRefusedError}. Its problem is the server's own when the
303
+ * wrapped error, which `uploadFile` passes as `cause`, answered a problem document. Otherwise it is
304
+ * `auth.invalid_credential` when the request carried a credential and `auth.missing_credential`
305
+ * when it carried none, as `options.credentialSent` says, and the wrapped error's reading of the
306
+ * status when the caller does not say. `filename` is the file whose upload was refused, as
263
307
  * `uploadFile` sent it.
264
308
  */
265
309
  export class UploadAuthenticationError extends InputPreparationError {
266
310
  status;
267
311
  filename;
268
312
  constructor(message, status, options) {
269
- super(message, { ...options, verdict: makeVerdict("config", false) });
313
+ super(message, {
314
+ cause: options?.cause,
315
+ problem: credentialProblem(options?.cause, options?.credentialSent),
316
+ });
270
317
  this.name = "UploadAuthenticationError";
271
318
  this.status = status;
272
319
  this.filename = options?.filename;
@@ -275,102 +322,114 @@ export class UploadAuthenticationError extends InputPreparationError {
275
322
  /**
276
323
  * The upload route refused a test file (`kind: "test"`) for what it claims, never for its bytes:
277
324
  * the caller's role may not change methods, or the method it names is unknown or being erased.
278
- * `code` says which; the SDK sets it on every one it raises, so it is undefined only on one a
325
+ * `reason` says which; the SDK sets it on every one it raises, so it is undefined only on one a
279
326
  * caller constructs without it. `status` is the HTTP status, `filename` the file `uploadFile` was
280
327
  * sending and `methodId` the method it named, and the wrapped `ApiResponseError` is reachable via
281
328
  * `cause`.
282
329
  *
283
- * Its verdict is the wrapped error's when `cause` carries one, the server's own; otherwise `code`
284
- * decides: `role_not_permitted` is `config`, since the caller's role must change, and the other
285
- * two are `input`, since the request must name another method; none is retryable.
330
+ * Its problem is the wrapped error's when `cause` carries one, the server's own. Otherwise it is
331
+ * `auth.forbidden` for `role_not_permitted`, naming a role that may change methods as the
332
+ * requirement, and `client.argument_invalid` naming `method_id` for the other two, the reason as
333
+ * the reason.
286
334
  */
287
335
  export class TestFileRefusedError extends InputPreparationError {
288
336
  status;
289
- code;
337
+ reason;
290
338
  filename;
291
339
  methodId;
292
340
  constructor(message, options) {
293
341
  super(message, {
294
- ...options,
295
- verdict: errorVerdictOf(options?.cause) ?? testFileRefusalVerdict(options?.code),
342
+ cause: options?.cause,
343
+ problem: wrappedProblem(options?.cause) ?? testFileRefusalProblem(options?.reason),
296
344
  });
297
345
  this.name = "TestFileRefusedError";
298
346
  this.status = options?.status;
299
- this.code = options?.code;
347
+ this.reason = options?.reason;
300
348
  this.filename = options?.filename;
301
349
  this.methodId = options?.methodId;
302
350
  }
303
351
  }
304
- /** The verdict of a test file's refusal that wraps no error carrying one. */
305
- function testFileRefusalVerdict(code) {
306
- // A role is changed by an admin of the organization, never by sending the request again.
307
- if (code === "role_not_permitted")
308
- return makeVerdict("config", false);
309
- return makeVerdict("input", false);
352
+ /** The problem of a test file's refusal that wraps no error carrying one; `undefined` leaves the family's own. */
353
+ function testFileRefusalProblem(reason) {
354
+ switch (reason) {
355
+ case "role_not_permitted":
356
+ return buildProblem("auth.forbidden", { requirement: "a role that may change methods" });
357
+ case "method_not_found":
358
+ case "method_being_deleted":
359
+ return argumentInvalidProblem("method_id", reason);
360
+ case undefined:
361
+ return undefined;
362
+ }
310
363
  }
311
364
  /**
312
365
  * A network or server fault reaching the upload route or storage — an unreachable
313
366
  * host, a timeout, a `5xx`, a refused redirect, storage timing out on the body,
314
367
  * storage timing out or throttling the request (a `408` or a `429`), or any other
315
- * unexpected `upload()` failure. `code` says which: the SDK sets it on
368
+ * unexpected `upload()` failure. `reason` says which: the SDK sets it on
316
369
  * every one it raises, so it is undefined only on one a caller constructs without
317
370
  * it. `status` is the HTTP status when a response produced it, and undefined when
318
371
  * none did. From `uploadFile` the wrapped `ApiResponseError` is also reachable via
319
372
  * `cause`, and `filename` is the file it was sending; `uploadWithGrant` wraps no response,
320
373
  * because storage's error body can echo the grant's credential, and leaves `filename` undefined.
321
374
  *
322
- * Its verdict is the wrapped error's when `cause` carries one: `uploadFile` wraps the
323
- * `ApiResponseError` or `ApiUnreachableError` the client's `upload()` threw, and `code` is too
324
- * coarse to judge it by (a `402` plan limit is `unexpected`, like a malformed answer). Otherwise
325
- * — always the case from `uploadWithGrant`, which wraps no response — `code` decides: `timeout`,
326
- * `storage_timeout` and `conflict` are `runtime` and retryable, `conflict` because storage
327
- * documents its `409 ConditionalRequestConflict` as retryable and the grant is not spent by it (a
328
- * spent hashless grant is the `412` of a `RejectedAssetError` with code `grant_used`); `server_error` is
329
- * `runtime`, and retryable when the fallback table a refused API request reads would call its
330
- * `status` retryable — any `5xx` but a `501`, which storage answers for a request it does not
331
- * implement — or when it carries no status; `unexpected` is `runtime`, and retryable when that
332
- * table would call its `status` retryable — storage's `408` or `429`, refused for its timing —
333
- * and not retryable for any other status or none; `unreachable` is `config` and retryable, like
334
- * `ApiUnreachableError`; `redirected` is `config` and not retryable; `invalid_grant_url` and no
335
- * code at all are `runtime` and not retryable.
375
+ * Its problem is `options.problem` when given, else the wrapped error's when `cause` carries one:
376
+ * `uploadFile` wraps the `ApiResponseError` or `ApiUnreachableError` the client's `upload()`
377
+ * threw. Otherwise — always the case from `uploadWithGrant`, which wraps no response — `reason`
378
+ * decides (`uploadTransportProblem`).
336
379
  */
337
380
  export class UploadTransportError extends InputPreparationError {
338
381
  status;
339
- code;
382
+ reason;
340
383
  filename;
341
384
  constructor(message, options) {
342
385
  super(message, {
343
- ...options,
344
- verdict: errorVerdictOf(options?.cause) ?? uploadTransportVerdict(options?.code, options?.status),
386
+ cause: options?.cause,
387
+ problem: options?.problem ??
388
+ wrappedProblem(options?.cause) ??
389
+ uploadTransportProblem(options?.reason, options?.status),
345
390
  });
346
391
  this.name = "UploadTransportError";
347
392
  this.status = options?.status;
348
- this.code = options?.code;
393
+ this.reason = options?.reason;
349
394
  this.filename = options?.filename;
350
395
  }
351
396
  }
352
- /** The verdict of an upload transport failure that wraps no error carrying one. */
353
- function uploadTransportVerdict(code, status) {
354
- switch (code) {
397
+ /**
398
+ * The problem of an upload transport failure that wraps no error carrying one, by its reason and
399
+ * the status storage answered:
400
+ *
401
+ * - `timeout`, `unreachable`, `storage_timeout` and `conflict` are `runtime.unavailable`: storage,
402
+ * a service the upload depends on, did not take the file, and a later attempt can (the SDK's
403
+ * own raise sites give `client.timeout` and `client.unreachable` instead, naming the limit and
404
+ * the host);
405
+ * - `server_error`, `unexpected` and no reason at all are `storageStatusProblem`'s reading of the
406
+ * status: `runtime.unavailable` for a `408`, a `429` or a `5xx` other than `501`,
407
+ * `runtime.unsupported` for a `501`, `client.response_unreadable` for any other status; a
408
+ * `server_error` with no status is `runtime.unavailable`, and the others `runtime.unexpected`;
409
+ * - `redirected` is `runtime.misconfigured`: storage redirecting a signed upload is a fault of the
410
+ * deployment;
411
+ * - `invalid_grant_url` is `client.response_unreadable`: the grant the API answered is unusable.
412
+ */
413
+ export function uploadTransportProblem(reason, status) {
414
+ switch (reason) {
355
415
  case "timeout":
416
+ case "unreachable":
356
417
  case "storage_timeout":
357
418
  case "conflict":
358
- return makeVerdict("runtime", true);
419
+ return buildProblem("runtime.unavailable", {});
359
420
  case "server_error":
360
- // A fault in storage may pass, but a 501 says storage does not implement the request, and
361
- // sending it again will not change that. The status reads as an API's would.
362
- return makeVerdict("runtime", status === undefined || fallbackVerdict(status, undefined, true).retryable);
421
+ return status === undefined
422
+ ? buildProblem("runtime.unavailable", {})
423
+ : storageStatusProblem(status);
363
424
  case "unexpected":
364
- // Storage's 408 or 429 refused the request for its timing, which a later attempt can pass;
365
- // any other status, or none, says nothing of the kind.
366
- return makeVerdict("runtime", status !== undefined && fallbackVerdict(status, undefined, true).retryable);
367
- case "unreachable":
368
- return makeVerdict("config", true);
425
+ case undefined:
426
+ return status === undefined
427
+ ? buildProblem("runtime.unexpected", {})
428
+ : storageStatusProblem(status);
369
429
  case "redirected":
370
- return makeVerdict("config", false);
371
- default:
372
- // `invalid_grant_url`, no code, and a code this version does not know.
373
- return makeVerdict("runtime", false);
430
+ return buildProblem("runtime.misconfigured", {});
431
+ case "invalid_grant_url":
432
+ return responseUnreadableProblem(200);
374
433
  }
375
434
  }
376
435
  /**
@@ -383,14 +442,15 @@ function uploadTransportVerdict(code, status) {
383
442
  * failures on the resolve route (`ApiResponseError`, `ApiUnreachableError`)
384
443
  * and the run-lifecycle errors propagate unchanged, so they are not subclasses.
385
444
  *
386
- * Its verdict is `runtime`, not retryable, unless `options.verdict` declares another: the SDK
387
- * declares `input` where it refuses an argument (a `scope`, a bound, a location) and `config`
388
- * where the environment refuses the download (a runtime with no filesystem, a directory that
389
- * cannot be created).
445
+ * Its problem is `options.problem`, else `runtime.unexpected`: the SDK gives
446
+ * `client.argument_invalid` where it refuses an argument (a `scope`, a bound, a location),
447
+ * `runtime.unsupported` for a runtime with no filesystem, `client.write_failed` for a directory
448
+ * that cannot be created, and `client.response_unreadable` for a resolution that answered the
449
+ * wrong count of items.
390
450
  */
391
451
  export class ArtifactOperationError extends PipelexRequestError {
392
452
  constructor(message, options) {
393
- super(message, options?.verdict ?? makeVerdict("runtime", false), options);
453
+ super(message, options?.problem ?? buildProblem("runtime.unexpected", {}), causeOption(options?.cause));
394
454
  this.name = "ArtifactOperationError";
395
455
  }
396
456
  }
@@ -399,14 +459,14 @@ export class ArtifactOperationError extends PipelexRequestError {
399
459
  * results: `main_stuff` or `working_memory` is `null`, or the key is missing
400
460
  * from the body altogether. Distinct from an empty walk over a present scope,
401
461
  * which is a produced verdict with no artifacts. `scope` names the scope,
402
- * `runId` the run.
462
+ * `runId` the run. Its problem is `client.response_unreadable`, naming the run.
403
463
  */
404
464
  export class ScopeUnavailableError extends ArtifactOperationError {
405
465
  scope;
406
466
  runId;
407
467
  constructor(scope, runId, options) {
408
468
  super(`Run "${runId}" carries no "${scope}" artifact to walk for produced files — ` +
409
- "it is null or absent from the results body.", options);
469
+ "it is null or absent from the results body.", { cause: options?.cause, problem: responseUnreadableProblem(200, runId) });
410
470
  this.name = "ScopeUnavailableError";
411
471
  this.scope = scope;
412
472
  this.runId = runId;
@@ -414,56 +474,70 @@ export class ScopeUnavailableError extends ArtifactOperationError {
414
474
  }
415
475
  /**
416
476
  * One reference could not be turned into a bounded response by `fetchArtifact`.
417
- * `code` says why, in a closed vocabulary the download verdict shares for its
418
- * per-item errors: the resolve route's own per-reference codes
477
+ * `reason` says why, in a closed vocabulary the download verdict shares for its
478
+ * per-item errors: the resolve route's own per-reference reasons
419
479
  * (`invalid_storage_uri`, `forbidden`), then the fetch boundary's —
420
480
  * `unsupported_url`, `plain_http_refused`, `redirect_refused`, `store_refused`
421
481
  * (a 401/403 from the object store), `not_found` (404/410), `store_error`
422
- * (any other non-2xx), `too_large`, `timeout`, `network`. `status` is the
423
- * store's HTTP status when one was received. `downloadArtifacts` never lets
424
- * this escape: it becomes the item's `error`.
425
- *
426
- * Its verdict follows `code`: `invalid_storage_uri`, `forbidden`, `unsupported_url`, `not_found`
427
- * and `too_large` are `input` and not retryable, since the reference or the bound must change;
428
- * `plain_http_refused` is `config` and not retryable; `redirect_refused` and `store_refused` are
429
- * `runtime` and not retryable; `timeout` and `network` are `runtime` and retryable; `store_error`
430
- * is `runtime`, and retryable when the fallback table a refused API request reads would call its
431
- * `status` retryable — a `408`, a `429` or a `5xx` other than `501` — and not retryable for any
432
- * other status or none. A code this version does not know is `runtime` and not retryable.
482
+ * (any other non-2xx), `too_large`, `timeout`, `network`, and `unknown` for a failure the SDK
483
+ * cannot place. `status` is the store's HTTP status when one was received. `downloadArtifacts`
484
+ * never lets this escape: it becomes the item's `error`.
485
+ *
486
+ * Its problem is `options.problem` when given, else follows `reason` and `status`
487
+ * (`artifactFetchProblem`). The SDK gives `client.download_limit_exceeded` naming the cap for
488
+ * `too_large`, `client.timeout` naming the budget for `timeout`, and `client.unreachable` naming
489
+ * the store's origin for `network`.
433
490
  */
434
491
  export class ArtifactFetchError extends ArtifactOperationError {
435
492
  uri;
436
- code;
493
+ reason;
437
494
  status;
438
- constructor(message, uri, code, status, options) {
439
- super(message, { ...options, verdict: artifactFetchVerdict(code, status) });
495
+ constructor(message, uri, reason, status, options) {
496
+ super(message, {
497
+ cause: options?.cause,
498
+ problem: options?.problem ?? artifactFetchProblem(reason, uri, status),
499
+ });
440
500
  this.name = "ArtifactFetchError";
441
501
  this.uri = uri;
442
- this.code = code;
502
+ this.reason = reason;
443
503
  this.status = status;
444
504
  }
445
505
  }
446
- /** The verdict of one reference's fetch failure, by its code and the store's status. */
447
- function artifactFetchVerdict(code, status) {
448
- switch (code) {
506
+ /**
507
+ * The problem of one reference's fetch failure that its raise site states none for, by its reason
508
+ * and the store's status: `input.reference_not_found` naming the reference for
509
+ * `invalid_storage_uri`, `forbidden` and `not_found`; `client.link_refused` for
510
+ * `unsupported_url`, `plain_http_refused` and `redirect_refused`; for `store_error` and
511
+ * `store_refused`, the object store failing or refusing a link the platform has just signed,
512
+ * `storageStatusProblem`'s reading of the store's status (`runtime.unavailable` for a `408`, a
513
+ * `429` or a `5xx` other than `501`, `runtime.unsupported` for a `501`, and
514
+ * `client.response_unreadable` naming any other status, a `401` or a `403` included), and
515
+ * `runtime.unavailable` when no status is known; `runtime.unavailable` for `timeout` and
516
+ * `network`; and `runtime.unexpected` otherwise, `unknown` and a reason this version does not know
517
+ * included.
518
+ */
519
+ export function artifactFetchProblem(reason, uri, status) {
520
+ switch (reason) {
449
521
  case "invalid_storage_uri":
450
522
  case "forbidden":
451
- case "unsupported_url":
452
523
  case "not_found":
453
- case "too_large":
454
- return makeVerdict("input", false);
524
+ return buildProblem("input.reference_not_found", { input: uri });
525
+ case "unsupported_url":
526
+ return buildProblem("client.link_refused", { reason: "unsupported_scheme" });
455
527
  case "plain_http_refused":
456
- return makeVerdict("config", false);
528
+ return buildProblem("client.link_refused", { reason: "plain_http" });
529
+ case "redirect_refused":
530
+ return buildProblem("client.link_refused", { reason: "redirect" });
531
+ case "store_error":
532
+ case "store_refused":
533
+ return status === undefined
534
+ ? buildProblem("runtime.unavailable", {})
535
+ : storageStatusProblem(status);
457
536
  case "timeout":
458
537
  case "network":
459
- return makeVerdict("runtime", true);
460
- case "store_error":
461
- // The store's status reads as an API's would: refused for its timing (408, 429) or a fault
462
- // that may pass (a 5xx) can succeed on a retry, while a 501 or a 4xx will not.
463
- return makeVerdict("runtime", status !== undefined && fallbackVerdict(status, undefined, true).retryable);
538
+ return buildProblem("runtime.unavailable", {});
464
539
  default:
465
- // `redirect_refused`, `store_refused`, and a code this version does not know.
466
- return makeVerdict("runtime", false);
540
+ return buildProblem("runtime.unexpected", {});
467
541
  }
468
542
  }
469
543
  /**
@@ -473,14 +547,19 @@ function artifactFetchVerdict(code, status) {
473
547
  * carries the result as it stood: every item saved before the refusal, and
474
548
  * the rest marked `aborted`. `status` is the route's status; the wrapped
475
549
  * `ApiResponseError` is reachable via `cause`. (`verdict` is the download's result, not the
476
- * error's own: that is `retryable` and `errorDomain`, `config` and not retryable, since the
477
- * credential must change.)
550
+ * error's own problem.) Its problem is the server's own when the wrapped error answered a problem
551
+ * document; otherwise `auth.invalid_credential`, or `auth.missing_credential` when
552
+ * `options.credentialSent` says the request carried no credential, and the wrapped error's reading
553
+ * of the status when the caller does not say.
478
554
  */
479
555
  export class ArtifactAuthenticationError extends ArtifactOperationError {
480
556
  status;
481
557
  verdict;
482
558
  constructor(message, status, verdict, options) {
483
- super(message, { ...options, verdict: makeVerdict("config", false) });
559
+ super(message, {
560
+ cause: options?.cause,
561
+ problem: credentialProblem(options?.cause, options?.credentialSent),
562
+ });
484
563
  this.name = "ArtifactAuthenticationError";
485
564
  this.status = status;
486
565
  this.verdict = verdict;
@@ -495,77 +574,88 @@ export class ArtifactAuthenticationError extends ArtifactOperationError {
495
574
  * `writeMethodTree` when a saved file could not be downloaded through its link, or its bytes are
496
575
  * not the file the listing names.
497
576
  *
498
- * `operation` says which way the file was moving, `code` why it failed, `sha256` which bytes,
577
+ * `operation` says which way the file was moving, `reason` why it failed, `sha256` which bytes,
499
578
  * `path` the tree path when one is known, and `status` storage's HTTP status when it answered.
500
579
  * Neither the message nor any member carries the link, whose query is a bearer credential, nor
501
580
  * storage's error body, which can echo the signed request, and it keeps no `cause`, since the
502
581
  * failure fetch throws can quote the URL.
503
582
  *
504
- * Its domain is always `runtime`, since neither the call nor the environment is at fault:
505
- * `unreachable`, `storage_error` and `mismatch` are retryable, since the same transfer can succeed
506
- * later, and `refused` is not. A download refused for an expired link succeeds once the method is
507
- * read again, which serves fresh links. A code this version does not know is not retryable.
583
+ * Its problem follows `reason` (`methodFileTransferProblem`): `unreachable` and `storage_error`
584
+ * are `runtime.unavailable`, since the same transfer can succeed later; `refused` is
585
+ * `storageStatusProblem`'s reading of storage's status (`client.response_unreadable` naming it, or
586
+ * `runtime.unsupported` for a `501`), a download refused for an expired link succeeding once the
587
+ * method is read again, which serves fresh links; and `mismatch` is `client.response_unreadable`.
508
588
  */
509
589
  export class MethodFileTransferError extends PipelexRequestError {
510
590
  operation;
511
- code;
591
+ reason;
512
592
  sha256;
513
593
  path;
514
594
  status;
515
595
  constructor(message, details) {
516
- super(message, methodFileTransferVerdict(details.code));
596
+ super(message, methodFileTransferProblem(details.reason, details.status));
517
597
  this.name = "MethodFileTransferError";
518
598
  this.operation = details.operation;
519
- this.code = details.code;
599
+ this.reason = details.reason;
520
600
  this.sha256 = details.sha256;
521
601
  this.path = details.path;
522
602
  this.status = details.status;
523
603
  }
524
604
  }
525
- /** The verdict of a failed transfer of a method's file, by its code. */
526
- function methodFileTransferVerdict(code) {
527
- switch (code) {
605
+ /** The problem of a failed transfer of a method's file, by its reason and storage's status. */
606
+ export function methodFileTransferProblem(reason, status) {
607
+ switch (reason) {
528
608
  case "unreachable":
529
609
  case "storage_error":
610
+ return buildProblem("runtime.unavailable", {});
611
+ case "refused":
612
+ return status === undefined
613
+ ? buildProblem("runtime.unexpected", {})
614
+ : storageStatusProblem(status);
530
615
  case "mismatch":
531
- return makeVerdict("runtime", true);
532
- default:
533
- // `refused`, and a code this version does not know.
534
- return makeVerdict("runtime", false);
616
+ return responseUnreadableProblem(200);
535
617
  }
536
618
  }
619
+ // ── Transport ────────────────────────────────────────────────────────
537
620
  /**
538
621
  * Thrown when the Pipelex API host cannot be reached at all (DNS failure,
539
622
  * connection refused, TLS handshake failure, request timeout). The HTTP
540
623
  * exchange never produced a response — distinguish from `ApiResponseError`,
541
- * which represents a non-2xx response that did come back.
624
+ * which represents a response that did come back, a body that arrived and could not be decoded
625
+ * included.
542
626
  *
543
- * `code` is the underlying network error code when available
627
+ * `networkCode` is the underlying network error code when available
544
628
  * (`ECONNREFUSED`, `ENOTFOUND`, `ETIMEDOUT`, `EAI_AGAIN`, `ABORT_TIMEOUT`).
545
629
  *
546
- * Its verdict is `config` and retryable: the address or the network must be checked, and a later
547
- * attempt can get through. With the code `ABORT_TIMEOUT`, the SDK's own request timeout, it is
548
- * `runtime` and retryable instead: the API took the request and did not answer in time, which is
549
- * no fault of the base URL.
550
- *
551
- * On a request that may create a run (`options.mayStartRun`, which the client sets on `start` and
552
- * `execute`), `runMayHaveStarted` is true unless `cause`, the failure fetch threw, proves the
553
- * request never left: the name lookup, the connection or the TLS handshake failed. A reset, a time
554
- * limit or a body that could not be decoded proves nothing, so the run may have started and the
555
- * error is not retryable.
630
+ * Its problem is the transport's: `client.timeout`, naming the time limit, for the SDK's own time
631
+ * limit (`ABORT_TIMEOUT`) when the client gives it as `options.timeoutMs`, and
632
+ * `client.unreachable`, naming the base URL, otherwise. Both have `retry` `yes`. On a request that
633
+ * may create a run (`options.mayStartRun`, which the client sets on `start` and `execute`), it is
634
+ * `client.outcome_unknown` instead, whose `retry` is `outcome_unknown`, unless `cause`, the failure
635
+ * fetch threw, proves the request never left: the name lookup, the connection or the TLS handshake
636
+ * failed. A reset or a time limit proves nothing, so the run may have started.
556
637
  */
557
638
  export class ApiUnreachableError extends PipelexRequestError {
558
639
  apiUrl;
559
- code;
560
- constructor(message, apiUrl, code, options) {
561
- super(message, makeVerdict(code === "ABORT_TIMEOUT" ? "runtime" : "config", true), {
562
- ...options,
563
- runMayHaveStarted: options?.mayStartRun === true && transportMayHaveStartedRun(options.cause),
564
- });
640
+ networkCode;
641
+ constructor(message, apiUrl, networkCode, options) {
642
+ super(message, transportProblem(apiUrl, networkCode, options), causeOption(options?.cause));
565
643
  this.name = "ApiUnreachableError";
566
644
  this.apiUrl = apiUrl;
567
- this.code = code;
645
+ this.networkCode = networkCode;
646
+ }
647
+ }
648
+ /** The SDK's own time limit ran out while the request was sent or answered. */
649
+ export const ABORT_TIMEOUT = "ABORT_TIMEOUT";
650
+ /** The problem of a request that got no answer, agreeing with whether it may have left. */
651
+ function transportProblem(apiUrl, networkCode, options) {
652
+ if (options?.mayStartRun === true && requestMayHaveLeft(options.cause)) {
653
+ return outcomeUnknownProblem();
654
+ }
655
+ if (networkCode === ABORT_TIMEOUT && options?.timeoutMs !== undefined) {
656
+ return buildProblem("client.timeout", { seconds: options.timeoutMs / 1000 });
568
657
  }
658
+ return buildProblem("client.unreachable", { base_url: apiUrl });
569
659
  }
570
660
  /**
571
661
  * Thrown when the blocking `execute` (`POST /v1/execute`) is killed by the
@@ -573,9 +663,9 @@ export class ApiUnreachableError extends PipelexRequestError {
573
663
  * run methods longer than 30s behind the hosted gateway — use the durable run
574
664
  * lifecycle (start + poll) instead.
575
665
  *
576
- * Its verdict is `input`, not retryable: asking again meets the same limit, and the caller fixes
577
- * it by starting the run and polling. `runMayHaveStarted` is always true: the gateway gave up on
578
- * the request, and the run it carried goes on without its id reaching the caller.
666
+ * Its problem is `client.outcome_unknown`: the gateway gave up on the request, and the run it
667
+ * carried may go on without its id reaching the caller, so it is looked up in the run history
668
+ * rather than sent again, and the next one is started and polled.
579
669
  */
580
670
  export class PipelineExecuteTimeoutError extends PipelexRequestError {
581
671
  elapsedMs;
@@ -583,62 +673,49 @@ export class PipelineExecuteTimeoutError extends PipelexRequestError {
583
673
  const seconds = Math.round(elapsedMs / 1000);
584
674
  super(`The Pipelex Hosted API times out synchronous requests after ~30s — this run took ${seconds}s. ` +
585
675
  "The blocking execute path can't run methods longer than 30s behind the gateway. " +
586
- "Start the run and poll for its result instead: `start()` then `waitForResult(runId)`.", makeVerdict("input", false), { ...options, runMayHaveStarted: true });
676
+ "Start the run and poll for its result instead: `start()` then `waitForResult(runId)`.", outcomeUnknownProblem(), causeOption(options?.cause));
587
677
  this.name = "PipelineExecuteTimeoutError";
588
678
  this.elapsedMs = elapsedMs;
589
679
  }
590
680
  }
681
+ // ── The run lifecycle ────────────────────────────────────────────────
682
+ /**
683
+ * The problem of a run that ended without success, `run.failed`, naming the run and its terminal
684
+ * status, with the run's own problem nested as `run_error` when there is one.
685
+ */
686
+ export function runFailedProblem(runId, status, runError) {
687
+ return buildProblem("run.failed", { pipeline_run_id: runId, run_status: status }, { pipeline_run_id: runId, extra: { run_error: runError ?? undefined } });
688
+ }
591
689
  /**
592
690
  * Thrown when a run reaches a terminal state that is not `COMPLETED` (`FAILED`, `CANCELLED`,
593
691
  * `TERMINATED`, `TIMED_OUT`) — by `waitForResult`, `startAndWaitForResult` and the artifact
594
692
  * download when the platform answers the results read with HTTP 409. (`getRunResult` returns
595
693
  * the same facts as its `failed` arm instead of throwing.)
596
694
  *
597
- * - `status` is the run's terminal status, read from the problem's `run_status` member, or from
598
- * its `detail` sentence on a platform that predates the member.
599
- * - `error` is the run's stored error report as the platform serves it, typed whole as
600
- * `RunErrorReport`: the runner's `error_type`, `message`, `title`, `type_uri`, `error_domain`,
601
- * `error_category`, `retryable`, `user_action`, `provider_metadata`, `caller_facing_message`,
602
- * `validation_errors` and anything newer through its index signature. Branch on
603
- * `error.error_domain`, `error.type_uri` and `error.retryable`; show `error.user_action` as the
604
- * next step. The platform serves the runner's STRICT projection, so `message` is the author's
605
- * copy only when `caller_facing_message` is `true` and the placeholder
606
- * `An internal error occurred.` otherwise, `user_action` comes only with caller-facing copy,
607
- * `provider_metadata` keeps only `status_code` and `retry_after_seconds`, and `model`,
608
- * `provider` and `migration` are absent; deciding what a person sees is still the consumer's.
609
- * A run the platform ended or settled itself carries the caller-facing report the platform
610
- * wrote, in the same shape. A cancelled, terminated or timed-out run carries a report when a
611
- * completion callback carried one, and a timed-out run otherwise the platform's timeout report.
612
- * `null` when the run ended with no stored report (one whose start failed, or one cancelled,
613
- * terminated or given its status by hand with no completion callback carrying a report).
614
- * - The error's own `message` is the problem's `detail`, which names the status and then the
615
- * report's message (`Run finished with status FAILED: <message>`), so printing the error
616
- * already tells the reason.
617
- * - `runId` locates the run, for a status read or a support request.
618
- *
619
- * Its verdict comes from the report. `errorDomain` is the report's `error_domain` when it is one
620
- * of the three domains, and `runtime` otherwise, including a run with no report. `retryable` is
621
- * true only when the report's `retryable` is `true`: a report that says nothing, and a run with
622
- * none, read as not retryable, since starting the run again spends credit and nothing says it
623
- * would succeed. The report keeps its own `retryable` as written, so a consumer that words the
624
- * unknown differently still can.
695
+ * - `problem` is `run.failed`, the results read's own problem document as the platform answered
696
+ * it, or the one the SDK builds for a failed run it learned of otherwise. Its `retry` is `no`:
697
+ * the run is over, and starting it again spends credit.
698
+ * - `runError` is the run's own problem, which `run.failed` nests as `run_error`: why the run did
699
+ * not succeed, under its own code (`run.cancelled`, `run.timed_out`, a model's or a method's
700
+ * code), with its own `domain`, `retry` and `next_step`. Branch on it to tell a run worth
701
+ * starting again from one whose method or inputs must change. `undefined` when the run ended
702
+ * with no problem stored.
703
+ * - `status` is the run's terminal status, `runId` the run.
625
704
  */
626
705
  export class RunFailedError extends PipelexRequestError {
627
706
  runId;
628
707
  status;
629
- error;
630
708
  constructor(message, runId, status, options) {
631
- super(message, runFailureVerdict(options?.error ?? null), options?.cause === undefined ? undefined : { cause: options.cause });
709
+ super(message, options?.problem ?? runFailedProblem(runId, status), causeOption(options?.cause));
632
710
  this.name = "RunFailedError";
633
711
  this.runId = runId;
634
712
  this.status = status;
635
- this.error = options?.error ?? null;
636
713
  }
637
- }
638
- /** The verdict of a failed run, read from its stored report. */
639
- function runFailureVerdict(report) {
640
- const domain = report?.error_domain;
641
- return makeVerdict(isErrorDomain(domain) ? domain : "runtime", report?.retryable === true);
714
+ /** The run's own problem, nested as `run_error`, when the run ended with one. */
715
+ get runError() {
716
+ const nested = this.problem.run_error;
717
+ return isProblem(nested) ? nested : undefined;
718
+ }
642
719
  }
643
720
  /**
644
721
  * Thrown when a completed run cannot deliver its main stuff.
@@ -650,28 +727,34 @@ function runFailureVerdict(report) {
650
727
  * memory. `runId` locates the run. (An empty-but-present main stuff — `{ items: [] }`, `{ text:
651
728
  * "" }` — is a valid output and does NOT throw; only a genuinely absent one does.)
652
729
  *
653
- * Its verdict is `runtime`, not retryable: the API broke its own contract on a completed run.
730
+ * Its problem is `client.response_unreadable`, naming the run: the API broke its own contract on a
731
+ * completed run.
654
732
  */
655
733
  export class MissingMainStuffError extends PipelexRequestError {
656
734
  runId;
657
735
  constructor(message, runId) {
658
- super(message, makeVerdict("runtime", false));
736
+ super(message, responseUnreadableProblem(200, runId));
659
737
  this.name = "MissingMainStuffError";
660
738
  this.runId = runId;
661
739
  }
662
740
  }
741
+ /** `client.follow_failed`: the client stopped following a run that may still be going. */
742
+ function followFailedProblem(runId) {
743
+ return buildProblem("client.follow_failed", { pipeline_run_id: runId }, { pipeline_run_id: runId });
744
+ }
663
745
  /**
664
746
  * Thrown when `waitForResult` exceeds its `timeoutMs` before the run reaches a
665
747
  * terminal state. The run is NOT cancelled — it keeps executing server-side and
666
748
  * can be resumed later by `runId` (the poll loop just stopped waiting).
667
749
  *
668
- * Its verdict is `runtime` and retryable: the run is still going, and waiting again can succeed.
750
+ * Its problem is `client.follow_failed`, naming the run: its `retry` is `outcome_unknown` and its
751
+ * step `read_run`, since the run goes on and is read by its id, never started again.
669
752
  */
670
753
  export class RunTimeoutError extends PipelexRequestError {
671
754
  runId;
672
755
  timeoutMs;
673
756
  constructor(message, runId, timeoutMs) {
674
- super(message, makeVerdict("runtime", true));
757
+ super(message, followFailedProblem(runId));
675
758
  this.name = "RunTimeoutError";
676
759
  this.runId = runId;
677
760
  this.timeoutMs = timeoutMs;
@@ -686,14 +769,15 @@ export class RunTimeoutError extends PipelexRequestError {
686
769
  * server-side — resume by `runId` (`getRunResult` / `waitForResult` on a
687
770
  * hosted deployment, or the `location` status resource when provided).
688
771
  *
689
- * Its verdict is `runtime` and retryable: the run is still going, and is resumed by its id.
772
+ * Its problem is `client.follow_failed`, naming the run: its `retry` is `outcome_unknown`, since
773
+ * the run exists and executing again would pay for a second one, and its step `read_run`.
690
774
  */
691
775
  export class RunStillRunningError extends PipelexRequestError {
692
776
  runId;
693
777
  retryAfterSeconds;
694
778
  location;
695
779
  constructor(message, runId, retryAfterSeconds = null, location = null, options) {
696
- super(message, makeVerdict("runtime", true), options);
780
+ super(message, followFailedProblem(runId), causeOption(options?.cause));
697
781
  this.name = "RunStillRunningError";
698
782
  this.runId = runId;
699
783
  this.retryAfterSeconds = retryAfterSeconds;
@@ -707,16 +791,17 @@ export class RunStillRunningError extends PipelexRequestError {
707
791
  * Run polling is a hosted-API extension, not part of the MTHDS Protocol: the
708
792
  * open-source `pipelex-api` runner executes methods but has no run store, so
709
793
  * it 404s those routes; only a deployment that includes the platform block
710
- * (the Pipelex Hosted API) serves status/results. Distinguished from a genuine
711
- * run-not-found 404, which carries the server's structured error envelope.
794
+ * (the Pipelex Hosted API) serves status/results. A route the deployment lacks answers a `404`
795
+ * whose body is no problem document; a run the platform does not know answers a `404` problem
796
+ * document, `run.not_found`, which is an `ApiResponseError`.
712
797
  *
713
- * Its verdict is `config`, not retryable: the base URL points at a bare runner without the run
714
- * lifecycle.
798
+ * Its problem is `runtime.unsupported`, naming the durable run lifecycle as the capability the
799
+ * deployment lacks: the base URL points at a bare runner without it.
715
800
  */
716
801
  export class RunLifecycleUnavailableError extends PipelexRequestError {
717
802
  apiUrl;
718
803
  constructor(message, apiUrl, options) {
719
- super(message, makeVerdict("config", false), options);
804
+ super(message, buildProblem("runtime.unsupported", { capability: "the durable run lifecycle" }), causeOption(options?.cause));
720
805
  this.name = "RunLifecycleUnavailableError";
721
806
  this.apiUrl = apiUrl;
722
807
  }
@@ -728,262 +813,69 @@ export class RunLifecycleUnavailableError extends PipelexRequestError {
728
813
  * The ceiling, `pageLimit` pages, sits far beyond any real catalog, run history or file list, so reaching it
729
814
  * is a server-side fault — an endpoint minting a fresh cursor forever — not a coverage limit the
730
815
  * caller can raise. Throwing beats returning, because a silently truncated list is exactly the bug
731
- * paging was introduced to remove. Its verdict is `runtime`, not retryable.
816
+ * paging was introduced to remove. Its problem is `client.response_unreadable`.
732
817
  */
733
818
  export class PagingNotTerminatingError extends PipelexRequestError {
734
819
  pageLimit;
735
820
  constructor(message, pageLimit) {
736
- super(message, makeVerdict("runtime", false));
821
+ super(message, responseUnreadableProblem(200));
737
822
  this.name = "PagingNotTerminatingError";
738
823
  this.pageLimit = pageLimit;
739
824
  }
740
825
  }
826
+ /** The response header carrying the request's correlation id. */
827
+ const REQUEST_ID_HEADER = "x-request-id";
828
+ /** A header's value, as fetch gives the headers or as a plain map of names. */
829
+ function headerValue(headers, name) {
830
+ if (headers === undefined)
831
+ return undefined;
832
+ if (typeof headers.get === "function") {
833
+ const value = headers.get(name);
834
+ return value === null || value === "" ? undefined : value;
835
+ }
836
+ const map = headers;
837
+ const found = Object.keys(map).find((key) => key.toLowerCase() === name);
838
+ const value = found === undefined ? undefined : map[found];
839
+ return value === undefined || value === "" ? undefined : value;
840
+ }
741
841
  /**
742
842
  * A response that DID come back from the API and that the SDK cannot hand back as a result: a
743
- * non-2xx refusal, with its problem document parsed, or a 2xx answer the SDK could not read —
744
- * a body that is not JSON, JSON that is not the object the route answers, or a stored method
745
- * whose `python` field is not the serialized file list. The second carries the answer's status
746
- * and raw text, no hosted problem member, what made it unreadable as `cause` (the parse failure,
747
- * or `mthds`'s refusal of the `python` field), the verdict the fallback gives a 2xx (`runtime`,
748
- * not retryable), and as `problem` the standard's reading of such an answer:
749
- * `client.response_unreadable`, or `client.outcome_unknown` on a request that may create a run.
750
- *
751
- * Every error the hosted API answers is an RFC 9457 `application/problem+json` document, and
752
- * this error carries its members as typed fields, each `undefined` when the document did not
753
- * carry it (a member of the wrong type reads as absent rather than as a wrong value, and the
754
- * nested members `providerMetadata`, `migration`, `validationErrors` and `errors` are checked
755
- * field by field as a failed run's stored report is) — except the verdict, which is always
756
- * decided:
757
- *
758
- * - **The verdict.** `errorDomain` says who can fix the failure — `input` (the caller),
759
- * `config` (a configuration change), `runtime` (nobody beforehand) — and `retryable` whether
760
- * asking again can succeed. Each is the document's member when the server sent a valid one,
761
- * and otherwise the SDK's fallback, read from the status, the platform `code` and whether the
762
- * body names what it refused. A runner often sends `error_domain` without `retryable`, and
763
- * then the domain it sent decides `retryable`: `input` and `config` are not retryable, as a
764
- * stored report that says nothing is not, since the caller or the environment must change,
765
- * while `runtime` takes the fallback's. Nothing on the error says which a value came from:
766
- * `problemDocument` keeps what the server sent.
767
- * - **A run that may have started.** On a `start` or an `execute`, `runMayHaveStarted` says
768
- * whether the answer leaves a run possibly started without its id reaching the caller: any
769
- * `2xx` the SDK could not read, a `408`, and every `5xx` but a `501` and the platform's `503`
770
- * coded `pipeline_start_unavailable`, which says the runner was never reached. Then
771
- * `retryable` is false, whatever the server sent: asking again could start a second run. A
772
- * runner's own failure of a blocking execute reads the same, since the SDK cannot tell a run
773
- * that ended from one that goes on, and its `retryable` stays readable on `problemDocument`.
774
- * On every other route it is false.
775
- * - **The branch fields.** `errorDomain`, and `type`, the stable URI naming the error class.
776
- * Branch on these, never on the HTTP status or on the wording of a message; they are the
777
- * fields that mean the same on both surfaces.
778
- * - **The native codes.** `code` is the platform's own closed code (`conflict`, `not_found`,
779
- * `pipelex_api_key_limit_reached`, …; `MethodErrorCode` names the method ones and
780
- * `IdempotencyErrorCode` the idempotency key's) and `errorType` the runner's open exception
781
- * class name. Each is finer than `errorDomain` and
782
- * specific to the surface that emits it, and a client of that surface may branch on it: the
783
- * platform's `type` is derived from its `code` one to one
784
- * (`https://pipelex.com/errors/<code>`), so the two read the same answer. A problem the
785
- * platform relays from the runner carries the runner's `type` and `errorType` and no `code`.
786
- * - **For a person.** `title` is the stable label of the error class, `serverMessage` the
787
- * per-occurrence `detail`, `userAction` the advised next step, and `errorCategory`, `model`,
788
- * `provider` and `providerMetadata` describe an inference failure.
789
- * - **For support.** `requestId` correlates the response with the server's logs; it is read
790
- * from the body, or from the `X-Request-ID` response header when the body has none.
791
- * `instance` is the occurrence's URN or request path.
792
- * - **Per-item failures.** `errors` is the platform's field-level list (`field`, `code`,
793
- * `detail`), and `validationErrors` the structured diagnostics of a bundle that failed
794
- * validation.
795
- * - **The standard's reading.** `problem` is the same answer read as the MTHDS standard reads
796
- * it, the problem `mthds`'s own `ApiResponseError` carries: the body itself when it is a core
797
- * problem document, and otherwise the problem its status reads as (`readAnswer` in
798
- * `mthds/protocol`), except on a request that may create a run, where `runMayHaveStarted`
799
- * decides: `client.outcome_unknown` for every answer that leaves a run possibly started, a core
800
- * problem document's body included. Branch on `problem.code` to handle this SDK's errors and the
801
- * standard client's alike. The hosted plane's problem documents predate the core registry, so
802
- * for them `problem` says little; the members above are read from the hosted document as they
803
- * always were, and those the standard names its own way do not collide with it: `code` is the
804
- * platform's code beside the registry's `problem.code`, and `errors` the platform's field
805
- * errors beside the problem items of `problem.errors`.
806
- *
807
- * `problemDocument` is the decoded document whole, so a member this SDK does not name stays
808
- * reachable without re-parsing `responseBody`, which is the raw text; it is `undefined` when
809
- * the body was not a JSON object.
810
- *
811
- * The error is a `PipelineRequestError`, as every error of this SDK is, rather than the
812
- * standard's `ProblemError`: a `ProblemError`'s `code` is the registry's and its message the
813
- * problem's sentence, while this error's `code` is the platform's and its message names the
814
- * route and the hosted document's `detail`, so it carries the standard's reading as `problem`
815
- * instead.
843
+ * non-2xx refusal, a body that arrived and could not be decoded, or a 2xx answer the SDK could not
844
+ * read — a body that is not JSON, JSON that is not the object the route answers, or a stored
845
+ * method whose `python` field is not the serialized file list.
846
+ *
847
+ * Its `problem` is the answer read as a problem (`readHostedAnswer`): the body itself when it is a
848
+ * problem document, as the hosted API answers every refusal, its `pipelex.*` code and any member
849
+ * the standard does not name kept; and otherwise the reading of the edge's bare answers by its
850
+ * status, `client.outcome_unknown` among them for a start or an execute the gateway cut off. Every
851
+ * getter of `ProblemError` reads it: branch on `code`, never on the HTTP status or the wording of
852
+ * a message. `retryAfter` is the problem's `retry_after`, which the `Retry-After` header fills when
853
+ * the problem invites a retry and gives none, and `requestId` the problem's `request_id`, else the
854
+ * answer's `X-Request-ID` header, the id to hand to support.
855
+ *
856
+ * `status` is the answer's HTTP status whatever the problem's own `status` says, `statusText` its
857
+ * reason phrase and `responseBody` its raw text, so a member this SDK does not name stays
858
+ * reachable without re-reading the body.
816
859
  */
817
860
  export class ApiResponseError extends PipelexRequestError {
818
861
  apiUrl;
819
862
  status;
820
863
  statusText;
821
864
  responseBody;
822
- errorType;
823
- serverMessage;
824
- /**
825
- * The platform's native code — a closed set (`conflict`, `not_found`, `run_not_found`,
826
- * `pipelex_api_key_limit_reached`, …), one-to-one with `type`; `MethodErrorCode` names the
827
- * ones about a stored method and its versions, and `IdempotencyErrorCode` the one about a
828
- * request's idempotency key. Finer than `errorDomain` and specific to the
829
- * platform: a runner's problem carries `errorType` instead. `undefined` for a body that
830
- * carries no `code`.
831
- */
832
- code;
833
- /**
834
- * RFC 9457 `type`: the stable URI naming the error class. With `errorDomain`, the field a
835
- * machine consumer branches on — the same class carries the same URI on every occurrence.
836
- */
837
- type;
838
- /** RFC 9457 `title`: the short human label of the error class. */
839
- title;
840
- /** RFC 9457 `instance`: the occurrence — the request path, or a request URN. */
841
- instance;
842
- /**
843
- * The request's correlation id — the body's `request_id`, or the `X-Request-ID` response
844
- * header when the body carries none. The id to hand to support: it finds the server's log
845
- * lines for this request.
846
- */
847
- requestId;
848
- /**
849
- * The body's `error_category`: the finer classification of an inference failure — known
850
- * values `transient`, `configuration`, `content`, `capacity`, `ambiguous`, `unknown`.
851
- */
852
- errorCategory;
853
- /** The body's `user_action`: what the caller should do next, when the server can say. */
854
- userAction;
855
- /** The body's `model`: the model an inference failure used. */
856
- model;
857
- /** The body's `provider`: the provider an inference failure reached. */
858
- provider;
859
- /**
860
- * The body's `provider_metadata`: what the provider's SDK said. On the hosted API it comes
861
- * from the runner's problem document, which the hosted runner renders under its STRICT
862
- * disclosure, so it keeps only `status_code` and `retry_after_seconds`; a runner that
863
- * discloses errors VERBOSE, as a self-hosted one does when `ERROR_DISCLOSURE` is unset, sends
864
- * it whole, the provider's raw text included.
865
- */
866
- providerMetadata;
867
- /** The body's `migration`: a pending configuration migration that explains the failure. */
868
- migration;
869
- /**
870
- * The platform's field-level `errors[]` — one item per offending request field. A refused
871
- * branding write names the logo slot or the colour key each entry is about in its `field`,
872
- * with a `LogoRefusalCode` for a logo the platform measured.
873
- */
874
- errors;
875
- /**
876
- * The decoded problem document whole — every member, named or not, such as a failed run's
877
- * `run_status` and `error` or a member a newer server adds. `undefined` when the body was not
878
- * a JSON object.
879
- */
880
- problemDocument;
881
- /**
882
- * Structured per-error diagnostics on a problem body that carries a top-level
883
- * `validation_errors[]` — the 422 a **run route** (`execute`, `start`) answers when the
884
- * runner refuses the method for its validation errors.
885
- *
886
- * Everywhere else an invalid bundle is a produced verdict, not an `ApiResponseError`:
887
- * `POST /v1/validate` answers it with a **200** `PipelexInvalidReport`, and the crate
888
- * routes with a **200** `CrateInvalidReport`, whose `validation_errors[]` the caller
889
- * reads off the returned value. This field is `undefined` for any error that carries no
890
- * per-error list (auth, transport, a request-shape 422, the 422 `pipe-io` answers for a
891
- * pipe selection it refuses). A consumer must NOT assume a given `error_type` implies a
892
- * populated list — fall back to `serverMessage` when this is empty.
893
- */
894
- validationErrors;
895
- /**
896
- * The answer read as the MTHDS standard reads it, the problem `mthds`'s own client carries,
897
- * agreeing with `runMayHaveStarted`: `client.outcome_unknown` where a run may have started,
898
- * whatever the body, and where none started the body when it is a core problem document,
899
- * otherwise the reading of an answer to a request that creates no run.
900
- * Built from the constructor's arguments, so an error a consumer builds by hand carries the
901
- * reading the client would give it.
902
- */
903
- problem;
904
- constructor(message, apiUrl, status, statusText, responseBody, errorType, serverMessage, validationErrors, code, options) {
905
- super(message, responseVerdict(status, errorType, code, options?.details), {
906
- ...(options?.cause === undefined ? {} : { cause: options.cause }),
907
- runMayHaveStarted: options?.mayStartRun === true && answerMayHaveStartedRun(status, code),
908
- });
865
+ requestIdHeader;
866
+ constructor(message, apiUrl, status, statusText, responseBody, options) {
867
+ super(message, options?.problem ??
868
+ readHostedAnswer({ status, headers: options?.headers ?? {}, body: responseBody }, { createsRun: options?.mayStartRun === true }).problem, causeOption(options?.cause));
909
869
  this.name = "ApiResponseError";
910
870
  this.apiUrl = apiUrl;
911
871
  this.status = status;
912
872
  this.statusText = statusText;
913
873
  this.responseBody = responseBody;
914
- this.errorType = errorType;
915
- this.serverMessage = serverMessage;
916
- this.validationErrors = validationErrors;
917
- this.code = code;
918
- const details = options?.details;
919
- this.type = details?.type;
920
- this.title = details?.title;
921
- this.instance = details?.instance;
922
- this.requestId = details?.requestId;
923
- this.errorCategory = details?.errorCategory;
924
- this.userAction = details?.userAction;
925
- this.model = details?.model;
926
- this.provider = details?.provider;
927
- this.providerMetadata = details?.providerMetadata;
928
- this.migration = details?.migration;
929
- this.errors = details?.errors;
930
- this.problemDocument = options?.problemDocument;
931
- this.problem = standardReading(status, responseBody, options?.headers ?? {}, options?.mayStartRun === true, code);
932
- }
933
- }
934
- /**
935
- * An answer read as the MTHDS standard reads it, as `mthds`'s own client does, agreeing with the
936
- * error's `runMayHaveStarted`: on a request that may create a run, the SDK's own reading of
937
- * whether a run may have started (`answerMayHaveStartedRun`) decides, as an
938
- * `ApiUnreachableError`'s does for a request that got no answer, so that `problem` never says a
939
- * run may exist where `runMayHaveStarted` says none started, nor the reverse.
940
- *
941
- * - Where a run may have started, the answer reads as `client.outcome_unknown`, naming the run
942
- * when the body names one (the platform's `bad_gateway` does when it lost a start's
943
- * acknowledgement), whatever the body: a `408`, which a proxy may answer for a request it
944
- * forwarded, every `5xx` but a `501` and a `503` carrying the platform's code
945
- * `pipeline_start_unavailable`, and a `2xx` the SDK could not read, since the server accepted
946
- * the request. A core problem document in such a body is not kept, since one inviting a retry
947
- * would contradict `runMayHaveStarted`.
948
- * - Where none started, which is every answer to a request that creates no run, a status outside
949
- * `2xx` whose body is a core problem document reads as that document, and any other as the
950
- * answer to a request that creates none: the code its status names (`runtime.unavailable` for
951
- * that `503`), else `client.response_unreadable` naming the status (that `501`). A `2xx` the SDK
952
- * could not read is `client.response_unreadable`.
953
- */
954
- function standardReading(status, body, headers, mayStartRun, code) {
955
- const runMayHaveStarted = mayStartRun && answerMayHaveStartedRun(status, code);
956
- if (!runMayHaveStarted) {
957
- if (status >= 200 && status < 300)
958
- return unreadableAnswer(status);
959
- return readAnswer({ status, headers, body }, { createsRun: false }).problem;
960
- }
961
- const reading = parseJsonBody(body);
962
- const runId = reading.ok && isPlainObject(reading.value) ? reading.value.pipeline_run_id : undefined;
963
- if (typeof runId !== "string" || runId === "") {
964
- return buildProblem("client.outcome_unknown", {});
965
- }
966
- return buildProblem("client.outcome_unknown", { pipeline_run_id: runId }, { pipeline_run_id: runId });
967
- }
968
- /**
969
- * The verdict of a refused request: each member the document's own when it is valid, and the
970
- * fallback's otherwise, except that a sent `input` or `config` domain with no sent `retryable` is
971
- * not retryable. A `404` is named when the body carries a platform `code` or a runner
972
- * `error_type`, read from the constructor's arguments, so an `ApiResponseError` a consumer builds
973
- * by hand gets the verdict the client would give it.
974
- */
975
- function responseVerdict(status, errorType, code, problem) {
976
- const fallback = fallbackVerdict(status, code, code !== undefined || errorType !== undefined);
977
- const sentDomain = problem?.errorDomain;
978
- const sentRetryable = problem?.retryable;
979
- if (!isErrorDomain(sentDomain)) {
980
- return makeVerdict(fallback.errorDomain, typeof sentRetryable === "boolean" ? sentRetryable : fallback.retryable);
981
- }
982
- if (typeof sentRetryable === "boolean")
983
- return makeVerdict(sentDomain, sentRetryable);
984
- // The server said who fixes the failure and nothing of a retry. When the caller or the
985
- // environment must change, asking again unchanged meets the same answer, whatever the status
986
- // would suggest; only a `runtime` fault keeps the status's reading.
987
- return makeVerdict(sentDomain, sentDomain === "runtime" ? fallback.retryable : false);
874
+ this.requestIdHeader = headerValue(options?.headers, REQUEST_ID_HEADER);
875
+ }
876
+ /** The request's id: the problem's `request_id`, else the answer's `X-Request-ID` header. */
877
+ get requestId() {
878
+ return super.requestId ?? this.requestIdHeader;
879
+ }
988
880
  }
989
881
  //# sourceMappingURL=errors.js.map