@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.
- package/README.md +20 -19
- package/dist/answer-reading.d.ts +70 -0
- package/dist/answer-reading.js +106 -0
- package/dist/answer-reading.js.map +1 -0
- package/dist/artifacts.js +45 -32
- package/dist/artifacts.js.map +1 -1
- package/dist/cli/present.d.ts +11 -14
- package/dist/cli/present.js +41 -37
- package/dist/cli/present.js.map +1 -1
- package/dist/cli/run.js +8 -7
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/script.d.ts +1 -1
- package/dist/cli/source.js +2 -2
- package/dist/cli/source.js.map +1 -1
- package/dist/client.d.ts +254 -213
- package/dist/client.js +542 -472
- package/dist/client.js.map +1 -1
- package/dist/codegen-check.d.ts +21 -15
- package/dist/codegen-check.js +54 -27
- package/dist/codegen-check.js.map +1 -1
- package/dist/errors.d.ts +298 -455
- package/dist/errors.js +426 -534
- package/dist/errors.js.map +1 -1
- package/dist/exact-json.d.ts +8 -12
- package/dist/exact-json.js +19 -12
- package/dist/exact-json.js.map +1 -1
- package/dist/hooks/check-core.d.ts +12 -7
- package/dist/hooks/check-core.js +20 -14
- package/dist/hooks/check-core.js.map +1 -1
- package/dist/hooks/validate-stage.js +12 -3
- package/dist/hooks/validate-stage.js.map +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/method-selector.js +7 -5
- package/dist/method-selector.js.map +1 -1
- package/dist/method-trees.d.ts +6 -7
- package/dist/method-trees.js +31 -37
- package/dist/method-trees.js.map +1 -1
- package/dist/models.d.ts +33 -210
- package/dist/models.js +2 -1
- package/dist/models.js.map +1 -1
- package/dist/prepare-inputs.d.ts +2 -2
- package/dist/prepare-inputs.js +51 -54
- package/dist/prepare-inputs.js.map +1 -1
- package/dist/problem-items.d.ts +19 -0
- package/dist/problem-items.js +52 -0
- package/dist/problem-items.js.map +1 -0
- package/dist/product-models.d.ts +43 -29
- package/dist/records.d.ts +12 -0
- package/dist/records.js +23 -0
- package/dist/records.js.map +1 -0
- package/dist/runs.d.ts +49 -38
- package/dist/runs.js +45 -19
- package/dist/runs.js.map +1 -1
- package/dist/transport-reading.d.ts +18 -0
- package/dist/{run-may-have-started.js → transport-reading.js} +15 -44
- package/dist/transport-reading.js.map +1 -0
- package/dist/upload-grant.d.ts +9 -6
- package/dist/upload-grant.js +42 -29
- package/dist/upload-grant.js.map +1 -1
- package/dist/upload.js +53 -33
- package/dist/upload.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +19 -19
- package/docs/artifact-download.md +6 -6
- package/docs/cli.md +1 -1
- package/docs/crate-routes.md +13 -14
- package/docs/errors.md +154 -267
- package/docs/exact-integers.md +1 -1
- package/docs/files.md +13 -13
- package/docs/hook-bundle.md +15 -1
- package/docs/input-preparation.md +14 -14
- package/docs/method-copy.md +35 -33
- package/docs/method-examples.md +13 -14
- package/docs/method-trees.md +27 -28
- package/docs/organization-branding.md +14 -16
- package/docs/run-results.md +8 -8
- package/package.json +1 -1
- package/dist/error-models.d.ts +0 -302
- package/dist/error-models.js +0 -203
- package/dist/error-models.js.map +0 -1
- package/dist/error-verdicts.d.ts +0 -46
- package/dist/error-verdicts.js +0 -120
- package/dist/error-verdicts.js.map +0 -1
- package/dist/run-may-have-started.d.ts +0 -31
- 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 {
|
|
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 (
|
|
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` `
|
|
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 `
|
|
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
|
|
172
|
-
//
|
|
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
|
|
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 {
|
|
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
|
-
|
|
219
|
-
if (
|
|
220
|
-
if (
|
|
221
|
-
if (
|
|
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`
|
|
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`.
|
|
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); //
|
|
238
|
-
console.error(err.
|
|
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
|
|
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
|
|
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)}.`,
|
|
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)}.`,
|
|
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)}.`,
|
|
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)}.`,
|
|
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)}.`,
|
|
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, {
|
|
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, {
|
|
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}`, {
|
|
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)}.`,
|
|
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).",
|
|
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)}.`, {
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
"
|
|
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
|