@pipelex/sdk 0.30.0 → 0.33.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 -4
- package/dist/artifacts.js +23 -9
- package/dist/artifacts.js.map +1 -1
- package/dist/client.d.ts +78 -12
- package/dist/client.js +234 -86
- package/dist/client.js.map +1 -1
- package/dist/codegen-check.d.ts +9 -0
- package/dist/codegen-check.js +12 -6
- package/dist/codegen-check.js.map +1 -1
- package/dist/error-models.d.ts +60 -2
- package/dist/error-models.js +169 -3
- package/dist/error-models.js.map +1 -1
- package/dist/error-verdicts.d.ts +34 -0
- package/dist/error-verdicts.js +69 -0
- package/dist/error-verdicts.js.map +1 -0
- package/dist/errors.d.ts +210 -46
- package/dist/errors.js +289 -62
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/prepare-inputs.js +26 -5
- package/dist/prepare-inputs.js.map +1 -1
- package/dist/upload-grant.d.ts +15 -10
- package/dist/upload-grant.js +36 -17
- package/dist/upload-grant.js.map +1 -1
- package/dist/upload.js +17 -4
- package/dist/upload.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +8 -7
- package/docs/artifact-download.md +3 -3
- package/docs/errors.md +93 -10
- package/docs/input-preparation.md +11 -7
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// client and the MTHDS runners cannot disagree about what they reject.
|
|
4
4
|
import { assertExclusiveRunSources, hasBundlePayload, parseMethodFiles, serializeMethodFiles, } from "mthds/protocol";
|
|
5
5
|
import { assertArtifactSelection, assertWaitOptions, pollUntilResult, selectionIncludesMainStuff, } from "./runs.js";
|
|
6
|
-
import { ApiResponseError, ApiUnreachableError, EmptyMethodSourceError, MissingMainStuffError, PipelineExecuteTimeoutError, PipelineRequestError, RunLifecycleUnavailableError, RunStillRunningError, } from "./errors.js";
|
|
6
|
+
import { ApiResponseError, ApiUnreachableError, EmptyMethodSourceError, MissingMainStuffError, PagingNotTerminatingError, PipelineExecuteTimeoutError, PipelineRequestError, RequestArgumentError, RunLifecycleUnavailableError, RunStillRunningError, } from "./errors.js";
|
|
7
|
+
import { isPlainObject, isValidationItem, readFieldError, readMigration, readProviderMetadata, readRunErrorReport, } from "./error-models.js";
|
|
7
8
|
import { methodSourceToContents } from "./method-source.js";
|
|
8
9
|
import { buildUserAgent } from "./user-agent.js";
|
|
9
10
|
import { uploadFile as uploadFileImpl } from "./upload.js";
|
|
@@ -44,10 +45,22 @@ const VALIDATE_MARKDOWN_RENDER_FORMAT = "markdown";
|
|
|
44
45
|
* `RunLifecycleUnavailableError` on the first poll.
|
|
45
46
|
*/
|
|
46
47
|
const BARE_RUNNER_IMPLEMENTATION = "pipelex-api";
|
|
47
|
-
/**
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Parse a wire method into the public shape (`python` → `MethodFile[]`). A stored `python` that
|
|
50
|
+
* is not the serialized `[{ name, content }]` list is server data the SDK cannot read, not a
|
|
51
|
+
* caller's argument, so `mthds`'s refusal is handed to `unreadable`, which builds the error to
|
|
52
|
+
* throw from the answer that carried it.
|
|
53
|
+
*/
|
|
54
|
+
function methodDataFromWire(wire, unreadable) {
|
|
49
55
|
const { python, ...rest } = wire;
|
|
50
|
-
|
|
56
|
+
if (python == null)
|
|
57
|
+
return rest;
|
|
58
|
+
try {
|
|
59
|
+
return { ...rest, python: parseMethodFiles(python) };
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
throw unreadable(err);
|
|
63
|
+
}
|
|
51
64
|
}
|
|
52
65
|
/**
|
|
53
66
|
* Serialize a write payload for the wire. `python` is three-way: OMITTED stays
|
|
@@ -82,11 +95,12 @@ export class PipelexApiClient {
|
|
|
82
95
|
// this constructor and must be held to that rule, or a path-prefixed value
|
|
83
96
|
// (e.g. `.../v1`) composes as `/v1/v1/...` and fails with a misleading
|
|
84
97
|
// endpoint error instead of a clear base-URL one. Trailing slashes are
|
|
85
|
-
// stripped first; a remaining path/query/fragment/credentials is rejected.
|
|
98
|
+
// stripped first; a remaining path/query/fragment/credentials is rejected. The
|
|
99
|
+
// refusal is `config`: the value typically comes from PIPELEX_BASE_URL, the environment.
|
|
86
100
|
if (!isValidBaseUrl(normalizedBaseUrl)) {
|
|
87
|
-
throw new
|
|
101
|
+
throw new RequestArgumentError(`Invalid API base URL "${normalizedBaseUrl}": must be host-only ` +
|
|
88
102
|
`(http/https, no path, query, fragment, or credentials). Endpoints ` +
|
|
89
|
-
`compose as {base}/v1/{endpoint}
|
|
103
|
+
`compose as {base}/v1/{endpoint}.`, { verdict: { errorDomain: "config", retryable: false } });
|
|
90
104
|
}
|
|
91
105
|
this.baseUrl = normalizedBaseUrl;
|
|
92
106
|
this.originUrl = new URL("/", this.baseUrl).origin;
|
|
@@ -182,25 +196,6 @@ export class PipelexApiClient {
|
|
|
182
196
|
body,
|
|
183
197
|
};
|
|
184
198
|
}
|
|
185
|
-
/**
|
|
186
|
-
* Issue a request and parse the JSON body, throwing a plain `Error` on a non-2xx
|
|
187
|
-
* response. Used only by `health` — the origin-level liveness probe, which sits
|
|
188
|
-
* outside `/v1` and outside the RFC 7807 error taxonomy the `/v1` routes share.
|
|
189
|
-
* Every `/v1` route goes through a helper that maps its problem body to the typed
|
|
190
|
-
* `ApiResponseError` instead.
|
|
191
|
-
*/
|
|
192
|
-
async requestJson(method, url, body) {
|
|
193
|
-
const res = await fetch(url, {
|
|
194
|
-
method,
|
|
195
|
-
headers: this.requestHeaders(body !== undefined),
|
|
196
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
197
|
-
});
|
|
198
|
-
if (!res.ok) {
|
|
199
|
-
const text = await res.text().catch(() => "");
|
|
200
|
-
throw new Error(`API ${method} ${url} failed (${res.status}): ${text || res.statusText}`);
|
|
201
|
-
}
|
|
202
|
-
return res.json();
|
|
203
|
-
}
|
|
204
199
|
/**
|
|
205
200
|
* Issue a Pipelex-product request (`/v1/me`, `/v1/methods`, `/v1/billing/*`,
|
|
206
201
|
* …) and parse its JSON body, mapping a non-2xx response to the typed
|
|
@@ -210,6 +205,59 @@ export class PipelexApiClient {
|
|
|
210
205
|
* timeout, not the blocking-execute ceiling.
|
|
211
206
|
*/
|
|
212
207
|
async requestProduct(method, endpoint, body, options = {}) {
|
|
208
|
+
const res = await this.requestProductAnswer(method, endpoint, body, options);
|
|
209
|
+
return (res.body ? this.readAnswer(method, endpoint, res) : undefined);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* A product request whose answer is an object the SDK or its caller reads before anything else
|
|
213
|
+
* — a bulk resolution's `items`, a run record's `error`, an upload grant's `url`. Unlike
|
|
214
|
+
* `requestProduct`, an empty
|
|
215
|
+
* 2xx is no answer here: it and a body that is not a JSON object throw the `ApiResponseError`
|
|
216
|
+
* of an answer the SDK cannot read (see `readObjectAnswerAt`).
|
|
217
|
+
*/
|
|
218
|
+
async requestProductObject(method, endpoint, body, options = {}) {
|
|
219
|
+
const res = await this.requestProductAnswer(method, endpoint, body, options);
|
|
220
|
+
return this.readObjectAnswer(method, endpoint, res);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* A methods-catalog route answering one stored method (`getMethod`, `createMethod`,
|
|
224
|
+
* `updateMethod`), parsed into the public shape. A stored `mthds` that is neither a string nor
|
|
225
|
+
* absent (`null` stays "no source", which `getMethodClosure` reports), and a stored `python`
|
|
226
|
+
* the SDK cannot parse, are an answer it cannot read, so each throws an `ApiResponseError` built
|
|
227
|
+
* from that answer, the latter with `mthds`'s refusal as `cause`: `runtime`, not retryable,
|
|
228
|
+
* since no change to the call fixes it.
|
|
229
|
+
*/
|
|
230
|
+
async requestMethodData(method, endpoint, body) {
|
|
231
|
+
const res = await this.requestProductAnswer(method, endpoint, body);
|
|
232
|
+
const wire = this.readObjectAnswer(method, endpoint, res);
|
|
233
|
+
const source = wire.mthds;
|
|
234
|
+
if (source != null && typeof source !== "string") {
|
|
235
|
+
throw this.unreadableAnswer(method, `/${API_PREFIX}/${endpoint}`, res, "a stored method whose `mthds` field is not a string");
|
|
236
|
+
}
|
|
237
|
+
return methodDataFromWire(wire, (cause) => this.unreadableAnswer(method, `/${API_PREFIX}/${endpoint}`, res, "a stored method whose `python` field could not be read", cause));
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* One page of a cursor-paged product route (`listMethods`, `listRuns`), read as the platform
|
|
241
|
+
* always serializes it: `items` an array, `next_cursor` a string or `null`. A page breaking
|
|
242
|
+
* either is an answer the SDK cannot read, so it throws the `ApiResponseError` `unreadableAnswer`
|
|
243
|
+
* builds, `runtime` and not retryable, rather than handing the iterators a page they cannot
|
|
244
|
+
* walk: `items` that is not iterable, or a missing cursor that is neither the end nor a next
|
|
245
|
+
* page, which `iterateRuns` would follow forever.
|
|
246
|
+
*/
|
|
247
|
+
async requestPage(endpoint) {
|
|
248
|
+
const res = await this.requestProductAnswer("GET", endpoint, undefined);
|
|
249
|
+
const page = this.readObjectAnswer("GET", endpoint, res);
|
|
250
|
+
const { items, next_cursor: nextCursor } = page;
|
|
251
|
+
if (!Array.isArray(items)) {
|
|
252
|
+
throw this.unreadableAnswer("GET", `/${API_PREFIX}/${endpoint}`, res, "a page whose `items` is not an array");
|
|
253
|
+
}
|
|
254
|
+
if (nextCursor !== null && typeof nextCursor !== "string") {
|
|
255
|
+
throw this.unreadableAnswer("GET", `/${API_PREFIX}/${endpoint}`, res, "a page whose `next_cursor` is neither a string nor null");
|
|
256
|
+
}
|
|
257
|
+
return { items: items, nextCursor };
|
|
258
|
+
}
|
|
259
|
+
/** Issue a product request and return its 2xx answer, a non-2xx thrown as `ApiResponseError`. */
|
|
260
|
+
async requestProductAnswer(method, endpoint, body, options = {}) {
|
|
213
261
|
const res = await this.requestRaw(method, this.url(endpoint), {
|
|
214
262
|
body,
|
|
215
263
|
timeoutMs: POLL_REQUEST_TIMEOUT_MS,
|
|
@@ -218,14 +266,64 @@ export class PipelexApiClient {
|
|
|
218
266
|
if (res.status < 200 || res.status >= 300) {
|
|
219
267
|
this.throwApiResponseError(method, endpoint, res);
|
|
220
268
|
}
|
|
221
|
-
return
|
|
269
|
+
return res;
|
|
222
270
|
}
|
|
223
271
|
throwApiResponseError(method, endpoint, res) {
|
|
272
|
+
this.throwApiResponseErrorAt(method, `/${API_PREFIX}/${endpoint}`, res);
|
|
273
|
+
}
|
|
274
|
+
/** The JSON body of a `/v1` route's 2xx answer; see `readAnswerAt`. */
|
|
275
|
+
readAnswer(method, endpoint, res) {
|
|
276
|
+
return this.readAnswerAt(method, `/${API_PREFIX}/${endpoint}`, res);
|
|
277
|
+
}
|
|
278
|
+
/** The JSON object body of a `/v1` route's 2xx answer; see `readObjectAnswerAt`. */
|
|
279
|
+
readObjectAnswer(method, endpoint, res) {
|
|
280
|
+
return this.readObjectAnswerAt(method, `/${API_PREFIX}/${endpoint}`, res);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The JSON body of a 2xx answer, naming the route by its `path` from the origin as a refusal
|
|
284
|
+
* does. A body that is not JSON, an empty one included, is an answer the SDK cannot read, so it
|
|
285
|
+
* throws the `ApiResponseError` `unreadableAnswer` builds, with the parse failure as `cause`.
|
|
286
|
+
* The one place a success body is parsed.
|
|
287
|
+
*/
|
|
288
|
+
readAnswerAt(method, path, res) {
|
|
289
|
+
try {
|
|
290
|
+
return JSON.parse(res.body);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
throw this.unreadableAnswer(method, path, res, res.body ? "a body that is not JSON" : "an empty body where JSON was expected", err);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The JSON body of a 2xx answer from a route that answers an object, read as `readAnswerAt`
|
|
298
|
+
* reads it. A body that is JSON but not an object — `null`, a number, a string, an array — is
|
|
299
|
+
* an answer the SDK cannot read either, and throws the same `ApiResponseError`, with no `cause`.
|
|
300
|
+
*/
|
|
301
|
+
readObjectAnswerAt(method, path, res) {
|
|
302
|
+
const answer = this.readAnswerAt(method, path, res);
|
|
303
|
+
if (!isPlainObject(answer)) {
|
|
304
|
+
throw this.unreadableAnswer(method, path, res, "a body that is not an object");
|
|
305
|
+
}
|
|
306
|
+
return answer;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* The `ApiResponseError` of a 2xx answer the SDK cannot read, its message
|
|
310
|
+
* `API <method> <path> answered <status> with <what>`: the answer's status, its raw text as
|
|
311
|
+
* `responseBody`, no problem member but the `X-Request-ID` header, the failure that made it
|
|
312
|
+
* unreadable as `cause`, and the verdict the fallback gives a 2xx, `runtime` and not retryable.
|
|
313
|
+
*/
|
|
314
|
+
unreadableAnswer(method, path, res, what, cause) {
|
|
315
|
+
return new ApiResponseError(`API ${method} ${path} answered ${res.status} with ${what}`, this.baseUrl, res.status, res.statusText, res.body, undefined, undefined, undefined, undefined, { cause, problem: { requestId: nonEmptyHeader(res.headers, REQUEST_ID_HEADER) } });
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Throw the `ApiResponseError` of a non-2xx answer, naming the route by its `path` from the
|
|
319
|
+
* origin (`/v1/...`, or `/health` for the liveness probe).
|
|
320
|
+
*/
|
|
321
|
+
throwApiResponseErrorAt(method, path, res) {
|
|
224
322
|
const { errorType, serverMessage, validationErrors, code, problem, document } = parseErrorBody(res.body);
|
|
225
323
|
// The body's `request_id` wins; the header is the fallback for a response whose body
|
|
226
324
|
// carries none (a gateway error page, a non-problem body).
|
|
227
325
|
const requestId = problem.requestId ?? nonEmptyHeader(res.headers, REQUEST_ID_HEADER);
|
|
228
|
-
throw new ApiResponseError(`API ${method}
|
|
326
|
+
throw new ApiResponseError(`API ${method} ${path} failed (${res.status}): ${serverMessage ?? (res.body || res.statusText)}`, this.baseUrl, res.status, res.statusText, res.body, errorType, serverMessage, validationErrors, code, { problem: { ...problem, requestId }, problemDocument: document });
|
|
229
327
|
}
|
|
230
328
|
/**
|
|
231
329
|
* Translate a "route absent" 404 (a bare pipelex-api with no platform block)
|
|
@@ -267,9 +365,24 @@ export class PipelexApiClient {
|
|
|
267
365
|
"running server-side. Poll its results (hosted) or use start().", runId, parseRetryAfter(res.headers), res.headers.get("location"));
|
|
268
366
|
}
|
|
269
367
|
// ── Health ────────────────────────────────────────────────────────
|
|
368
|
+
/**
|
|
369
|
+
* The origin-level liveness probe — `GET /health`, which sits at the origin, NOT under the
|
|
370
|
+
* `/v1` prefix. A bare runner serves it; a hosted origin answers `/v1/health` instead, so
|
|
371
|
+
* there this throws.
|
|
372
|
+
*
|
|
373
|
+
* It goes through the same transport as every route: an unreachable origin is an
|
|
374
|
+
* `ApiUnreachableError`, and a non-2xx answer an `ApiResponseError` whose verdict the SDK's
|
|
375
|
+
* fallback reads from the status, since the probe answers no problem document. So is a 2xx
|
|
376
|
+
* whose body is not a JSON object, such as a gateway's HTML page: `runtime`, not retryable.
|
|
377
|
+
*/
|
|
270
378
|
async health() {
|
|
271
|
-
|
|
272
|
-
|
|
379
|
+
const res = await this.requestRaw("GET", `${this.originUrl}/health`, {
|
|
380
|
+
timeoutMs: POLL_REQUEST_TIMEOUT_MS,
|
|
381
|
+
});
|
|
382
|
+
if (res.status < 200 || res.status >= 300) {
|
|
383
|
+
this.throwApiResponseErrorAt("GET", "/health", res);
|
|
384
|
+
}
|
|
385
|
+
return this.readObjectAnswerAt("GET", "/health", res);
|
|
273
386
|
}
|
|
274
387
|
// ── Protocol surface ─────────────────────────────────────────────────
|
|
275
388
|
/**
|
|
@@ -295,9 +408,9 @@ export class PipelexApiClient {
|
|
|
295
408
|
Object.keys(api).length === 0 &&
|
|
296
409
|
Object.keys(hosted).length === 0 &&
|
|
297
410
|
Object.keys(extensions).length === 0) {
|
|
298
|
-
throw new
|
|
411
|
+
throw new RequestArgumentError("Either pipe_code, mthds_contents, a method bundle (files/bundle_b64), a method_ref, a hosted method_id or a server-specific extension arg (extra) must be provided to execute().");
|
|
299
412
|
}
|
|
300
|
-
|
|
413
|
+
assertRunSourcesExclusive(options);
|
|
301
414
|
assertMethodRefPairsWithNothing(options);
|
|
302
415
|
const request = {
|
|
303
416
|
pipe_code: options.pipe_code,
|
|
@@ -323,7 +436,7 @@ export class PipelexApiClient {
|
|
|
323
436
|
}
|
|
324
437
|
// Wrap the base result in the enriched subtype (adds the `.main_stuff` accessor; the
|
|
325
438
|
// `main_stuff_name` extension + working memory ride `pipe_output`).
|
|
326
|
-
return new PipelexExecuteResult(
|
|
439
|
+
return new PipelexExecuteResult(this.readObjectAnswer("POST", "execute", res));
|
|
327
440
|
}
|
|
328
441
|
catch (err) {
|
|
329
442
|
if (err instanceof RunStillRunningError)
|
|
@@ -361,9 +474,9 @@ export class PipelexApiClient {
|
|
|
361
474
|
Object.keys(api).length === 0 &&
|
|
362
475
|
Object.keys(hosted).length === 0 &&
|
|
363
476
|
Object.keys(extensions).length === 0) {
|
|
364
|
-
throw new
|
|
477
|
+
throw new RequestArgumentError("Either pipe_code, mthds_contents, a method bundle (files/bundle_b64), a method_ref, a hosted method_id or a server-specific extension arg (extra) must be provided to start().");
|
|
365
478
|
}
|
|
366
|
-
|
|
479
|
+
assertRunSourcesExclusive(options);
|
|
367
480
|
assertMethodRefPairsWithNothing(options);
|
|
368
481
|
// `?? undefined` so JSON.stringify drops absent fields from the wire body.
|
|
369
482
|
const request = {
|
|
@@ -401,7 +514,7 @@ export class PipelexApiClient {
|
|
|
401
514
|
if (res.status < 200 || res.status >= 300) {
|
|
402
515
|
this.throwApiResponseError("POST", "start", res);
|
|
403
516
|
}
|
|
404
|
-
return
|
|
517
|
+
return this.readObjectAnswer("POST", "start", res);
|
|
405
518
|
}
|
|
406
519
|
/**
|
|
407
520
|
* Parse, validate, and dry-run an MTHDS bundle — `POST /v1/validate`.
|
|
@@ -480,19 +593,19 @@ export class PipelexApiClient {
|
|
|
480
593
|
// A selector object. The illegal shapes are compile errors for typed
|
|
481
594
|
// callers (`ValidateMethodSelector` pins the other key to `never`); the
|
|
482
595
|
// runtime checks back them for untyped (JS) callers — a typed
|
|
483
|
-
// `
|
|
596
|
+
// `RequestArgumentError`, never a native TypeError off a null source —
|
|
484
597
|
// mirroring the server's strict tooling XOR instead of silently picking.
|
|
485
598
|
if (source === null || source === undefined || typeof source !== "object") {
|
|
486
|
-
throw new
|
|
599
|
+
throw new RequestArgumentError("validate() takes inline contents (a string[]) or a method selector object " +
|
|
487
600
|
"({ method_ref } or { method_id }).");
|
|
488
601
|
}
|
|
489
602
|
const methodRef = nonEmptyString(source.method_ref);
|
|
490
603
|
const methodId = nonEmptyString(source.method_id);
|
|
491
604
|
if ((methodRef === undefined) === (methodId === undefined)) {
|
|
492
|
-
throw new
|
|
605
|
+
throw new RequestArgumentError("validate() takes exactly one method selector: inline contents, { method_ref }, or { method_id }.");
|
|
493
606
|
}
|
|
494
607
|
if (mthdsSources !== undefined) {
|
|
495
|
-
throw new
|
|
608
|
+
throw new RequestArgumentError("mthds_sources labels inline mthds_contents; a method_ref / method_id validation gets " +
|
|
496
609
|
"its source labels from the package's (or the stored method's) real file names.");
|
|
497
610
|
}
|
|
498
611
|
if (methodRef !== undefined)
|
|
@@ -512,7 +625,7 @@ export class PipelexApiClient {
|
|
|
512
625
|
if (res.status < 200 || res.status >= 300) {
|
|
513
626
|
this.throwApiResponseError("POST", "validate", res);
|
|
514
627
|
}
|
|
515
|
-
return
|
|
628
|
+
return this.readObjectAnswer("POST", "validate", res);
|
|
516
629
|
}
|
|
517
630
|
/**
|
|
518
631
|
* Validate paired MTHDS files while preserving URI attribution for diagnostics.
|
|
@@ -524,7 +637,7 @@ export class PipelexApiClient {
|
|
|
524
637
|
*/
|
|
525
638
|
async validateFiles(files, options = {}) {
|
|
526
639
|
if (files.length === 0) {
|
|
527
|
-
throw new
|
|
640
|
+
throw new RequestArgumentError("At least one MTHDS file must be provided to validateFiles().");
|
|
528
641
|
}
|
|
529
642
|
const mthdsContents = files.map((file) => file.content);
|
|
530
643
|
const hasAnyUri = files.some((file) => file.uri !== undefined);
|
|
@@ -638,7 +751,7 @@ export class PipelexApiClient {
|
|
|
638
751
|
if (res.status < 200 || res.status >= 300) {
|
|
639
752
|
this.throwApiResponseError("POST", endpoint, res);
|
|
640
753
|
}
|
|
641
|
-
return
|
|
754
|
+
return this.readObjectAnswer("POST", endpoint, res);
|
|
642
755
|
}
|
|
643
756
|
/** The model deck the runner can route to — `GET /v1/models[?type=]`. */
|
|
644
757
|
async models(category) {
|
|
@@ -649,7 +762,7 @@ export class PipelexApiClient {
|
|
|
649
762
|
if (res.status < 200 || res.status >= 300) {
|
|
650
763
|
this.throwApiResponseError("GET", endpoint, res);
|
|
651
764
|
}
|
|
652
|
-
return
|
|
765
|
+
return this.readObjectAnswer("GET", endpoint, res);
|
|
653
766
|
}
|
|
654
767
|
/**
|
|
655
768
|
* Protocol + implementation versions — `GET /v1/version` (always public).
|
|
@@ -662,7 +775,7 @@ export class PipelexApiClient {
|
|
|
662
775
|
if (res.status < 200 || res.status >= 300) {
|
|
663
776
|
this.throwApiResponseError("GET", "version", res);
|
|
664
777
|
}
|
|
665
|
-
return
|
|
778
|
+
return this.readObjectAnswer("GET", "version", res);
|
|
666
779
|
}
|
|
667
780
|
// ── Crate extensions (Pipelex API — `/v1/resolve`, `/v1/codegen`, `/v1/pipe-io`) ──
|
|
668
781
|
//
|
|
@@ -776,8 +889,10 @@ export class PipelexApiClient {
|
|
|
776
889
|
* Self-healing: a finished-but-unrecorded run resolves to its true terminal
|
|
777
890
|
* status on read. `degraded: true` means Temporal was unreachable and
|
|
778
891
|
* `status` is the last-known value; `retry_after_seconds` carries the
|
|
779
|
-
* server's backoff hint when present.
|
|
780
|
-
*
|
|
892
|
+
* server's backoff hint when present. A failed run's stored report, `error`,
|
|
893
|
+
* is checked field by field (see `RunErrorReport`). Throws
|
|
894
|
+
* `RunLifecycleUnavailableError` when the lifecycle routes are absent (a bare
|
|
895
|
+
* runner).
|
|
781
896
|
*/
|
|
782
897
|
async getRunStatus(runId, options = {}) {
|
|
783
898
|
const endpoint = `${RUNS}/${encodeURIComponent(runId)}/status`;
|
|
@@ -790,7 +905,7 @@ export class PipelexApiClient {
|
|
|
790
905
|
if (res.status < 200 || res.status >= 300) {
|
|
791
906
|
this.throwApiResponseError("GET", endpoint, res);
|
|
792
907
|
}
|
|
793
|
-
const run =
|
|
908
|
+
const run = withCheckedReport(this.readObjectAnswer("GET", endpoint, res));
|
|
794
909
|
const retryAfter = parseRetryAfter(res.headers);
|
|
795
910
|
return retryAfter !== null ? { ...run, retry_after_seconds: retryAfter } : run;
|
|
796
911
|
}
|
|
@@ -843,7 +958,7 @@ export class PipelexApiClient {
|
|
|
843
958
|
if (res.status < 200 || res.status >= 300) {
|
|
844
959
|
this.throwApiResponseError("GET", endpoint, res);
|
|
845
960
|
}
|
|
846
|
-
const result =
|
|
961
|
+
const result = this.readObjectAnswer("GET", endpoint, res);
|
|
847
962
|
if (selectionIncludesMainStuff(options.artifacts) && result.main_stuff == null) {
|
|
848
963
|
throw new MissingMainStuffError(`Completed run '${runId}' returned no main stuff — a completed run always delivers a main stuff.`, runId);
|
|
849
964
|
}
|
|
@@ -944,8 +1059,7 @@ export class PipelexApiClient {
|
|
|
944
1059
|
if (query.cursor !== undefined)
|
|
945
1060
|
params.set("cursor", query.cursor);
|
|
946
1061
|
const suffix = params.toString();
|
|
947
|
-
|
|
948
|
-
return { items: page.items, nextCursor: page.next_cursor };
|
|
1062
|
+
return this.requestPage(suffix ? `methods?${suffix}` : "methods");
|
|
949
1063
|
}
|
|
950
1064
|
/**
|
|
951
1065
|
* Every method in the catalog, streamed — follows the cursor for you.
|
|
@@ -997,16 +1111,15 @@ export class PipelexApiClient {
|
|
|
997
1111
|
// partial answer with no error is back to the original bug one layer
|
|
998
1112
|
// up; an error is the only honest response to a server that will not
|
|
999
1113
|
// finish. Unreachable on real data — see MAX_PAGES.
|
|
1000
|
-
throw new
|
|
1001
|
-
`This is a server-side fault, not a coverage limit
|
|
1114
|
+
throw new PagingNotTerminatingError(`listMethods did not terminate after ${MAX_PAGES} pages; refusing to keep paging. ` +
|
|
1115
|
+
`This is a server-side fault, not a coverage limit.`, MAX_PAGES);
|
|
1002
1116
|
}
|
|
1003
1117
|
cursor = page.nextCursor;
|
|
1004
1118
|
}
|
|
1005
1119
|
}
|
|
1006
1120
|
/** Fetch one method by id — `GET /v1/methods/{id}`. */
|
|
1007
1121
|
async getMethod(methodId) {
|
|
1008
|
-
|
|
1009
|
-
return methodDataFromWire(wire);
|
|
1122
|
+
return this.requestMethodData("GET", `methods/${encodeURIComponent(methodId)}`);
|
|
1010
1123
|
}
|
|
1011
1124
|
/**
|
|
1012
1125
|
* Resolve a stored method's id into its runnable MTHDS closure — a client-side
|
|
@@ -1038,13 +1151,11 @@ export class PipelexApiClient {
|
|
|
1038
1151
|
}
|
|
1039
1152
|
/** Create a method — `POST /v1/methods`. */
|
|
1040
1153
|
async createMethod(input) {
|
|
1041
|
-
|
|
1042
|
-
return methodDataFromWire(wire);
|
|
1154
|
+
return this.requestMethodData("POST", "methods", methodWriteToWire(input));
|
|
1043
1155
|
}
|
|
1044
1156
|
/** Replace a method (rename = changed `name`) — `PUT /v1/methods/{id}`. */
|
|
1045
1157
|
async updateMethod(methodId, input) {
|
|
1046
|
-
|
|
1047
|
-
return methodDataFromWire(wire);
|
|
1158
|
+
return this.requestMethodData("PUT", `methods/${encodeURIComponent(methodId)}`, methodWriteToWire(input));
|
|
1048
1159
|
}
|
|
1049
1160
|
/**
|
|
1050
1161
|
* Erase a method and everything it produced — `DELETE /v1/methods/{id}`.
|
|
@@ -1149,7 +1260,7 @@ export class PipelexApiClient {
|
|
|
1149
1260
|
* only: a deployment without the route answers a `404` `ApiResponseError`.
|
|
1150
1261
|
*/
|
|
1151
1262
|
async resolveStorageUrls(input, options = {}) {
|
|
1152
|
-
return this.
|
|
1263
|
+
return this.requestProductObject("POST", "resolve-storage-url/bulk", input, options);
|
|
1153
1264
|
}
|
|
1154
1265
|
/**
|
|
1155
1266
|
* Resolve a whole list of `pipelex-storage://` references through the bulk
|
|
@@ -1182,9 +1293,18 @@ export class PipelexApiClient {
|
|
|
1182
1293
|
async downloadArtifacts(request) {
|
|
1183
1294
|
return downloadArtifactsImpl(this, request);
|
|
1184
1295
|
}
|
|
1185
|
-
/**
|
|
1296
|
+
/**
|
|
1297
|
+
* Upload a base64 file — `POST /v1/upload`. An answer with no non-empty string `uri` names no
|
|
1298
|
+
* stored file, so it is an answer the SDK cannot read: an `ApiResponseError`, `runtime` and
|
|
1299
|
+
* not retryable, which `uploadFile` wraps as an `UploadTransportError` with code `unexpected`.
|
|
1300
|
+
*/
|
|
1186
1301
|
async upload(input) {
|
|
1187
|
-
|
|
1302
|
+
const res = await this.requestProductAnswer("POST", "upload", input);
|
|
1303
|
+
const uploaded = this.readObjectAnswer("POST", "upload", res);
|
|
1304
|
+
if (typeof uploaded.uri !== "string" || uploaded.uri === "") {
|
|
1305
|
+
throw this.unreadableAnswer("POST", `/${API_PREFIX}/upload`, res, "an answer with no string `uri`");
|
|
1306
|
+
}
|
|
1307
|
+
return uploaded;
|
|
1188
1308
|
}
|
|
1189
1309
|
/**
|
|
1190
1310
|
* Request a grant to upload one file straight to storage — `POST /v1/upload/grant`.
|
|
@@ -1203,7 +1323,7 @@ export class PipelexApiClient {
|
|
|
1203
1323
|
* `docs/input-preparation.md`.
|
|
1204
1324
|
*/
|
|
1205
1325
|
async requestUploadGrant(input, options = {}) {
|
|
1206
|
-
return this.
|
|
1326
|
+
return this.requestProductObject("POST", "upload/grant", input, options);
|
|
1207
1327
|
}
|
|
1208
1328
|
/**
|
|
1209
1329
|
* Upload one local asset and return its {@link UploadRecord} — the single-asset
|
|
@@ -1277,8 +1397,8 @@ export class PipelexApiClient {
|
|
|
1277
1397
|
params.set("limit", String(query.limit));
|
|
1278
1398
|
if (query.cursor !== undefined)
|
|
1279
1399
|
params.set("cursor", query.cursor);
|
|
1280
|
-
const page = await this.
|
|
1281
|
-
return { items: page.items, nextCursor: page.
|
|
1400
|
+
const page = await this.requestPage(`runs?${params.toString()}`);
|
|
1401
|
+
return { items: page.items.map(withCheckedReport), nextCursor: page.nextCursor };
|
|
1282
1402
|
}
|
|
1283
1403
|
/**
|
|
1284
1404
|
* Every run of a method, streamed — follows the cursor for you.
|
|
@@ -1333,10 +1453,11 @@ export class PipelexApiClient {
|
|
|
1333
1453
|
*
|
|
1334
1454
|
* The ONLY call that returns `mthds_contents` (what the run actually
|
|
1335
1455
|
* executed) and `inputs`. Kept off the status read, which pollers hit every
|
|
1336
|
-
* few seconds.
|
|
1456
|
+
* few seconds. A failed run's stored report, `error`, is checked field by
|
|
1457
|
+
* field (see `RunErrorReport`).
|
|
1337
1458
|
*/
|
|
1338
1459
|
async getRunDetail(runId) {
|
|
1339
|
-
return this.
|
|
1460
|
+
return withCheckedReport(await this.requestProductObject("GET", `runs/${encodeURIComponent(runId)}`));
|
|
1340
1461
|
}
|
|
1341
1462
|
/** Patch a run's status (admin/manual) — `PUT /v1/runs/{id}`. */
|
|
1342
1463
|
async updateRun(runId, input) {
|
|
@@ -1458,7 +1579,7 @@ function buildExtensions(extra) {
|
|
|
1458
1579
|
const snapshot = { ...extra };
|
|
1459
1580
|
const reserved = Object.keys(snapshot).filter((key) => RESERVED_EXTRA_KEYS.has(key));
|
|
1460
1581
|
if (reserved.length > 0) {
|
|
1461
|
-
throw new
|
|
1582
|
+
throw new RequestArgumentError(`extra carries reserved request args [${reserved.sort().join(", ")}] — pass them as named options instead.`);
|
|
1462
1583
|
}
|
|
1463
1584
|
const result = {};
|
|
1464
1585
|
for (const [key, value] of Object.entries(snapshot)) {
|
|
@@ -1508,6 +1629,23 @@ function buildHostedRunExtensions(options) {
|
|
|
1508
1629
|
const methodId = nonEmptyString(options.method_id);
|
|
1509
1630
|
return methodId === undefined ? {} : { method_id: methodId };
|
|
1510
1631
|
}
|
|
1632
|
+
/**
|
|
1633
|
+
* The standard's run-source exclusivity check (`assertExclusiveRunSources`: the two bundle
|
|
1634
|
+
* encodings together, or a bundle beside inline contents), its refusal rethrown as a
|
|
1635
|
+
* `RequestArgumentError` with the same message and the standard's error as `cause`, so it carries
|
|
1636
|
+
* a verdict like every other argument the client refuses.
|
|
1637
|
+
*/
|
|
1638
|
+
function assertRunSourcesExclusive(options) {
|
|
1639
|
+
try {
|
|
1640
|
+
assertExclusiveRunSources(options);
|
|
1641
|
+
}
|
|
1642
|
+
catch (err) {
|
|
1643
|
+
if (err instanceof PipelineRequestError) {
|
|
1644
|
+
throw new RequestArgumentError(err.message, { cause: err });
|
|
1645
|
+
}
|
|
1646
|
+
throw err;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1511
1649
|
/**
|
|
1512
1650
|
* Enforce the run routes' `method_ref` exclusivity, mirroring the server's own
|
|
1513
1651
|
* 422s so an illegal pairing fails before anything hits the wire. A
|
|
@@ -1528,13 +1666,13 @@ function assertMethodRefPairsWithNothing(options) {
|
|
|
1528
1666
|
if (nonEmptyString(options.method_ref) === undefined)
|
|
1529
1667
|
return;
|
|
1530
1668
|
if (options.mthds_contents != null && options.mthds_contents.length > 0) {
|
|
1531
|
-
throw new
|
|
1669
|
+
throw new RequestArgumentError("method_ref and inline mthds_contents are mutually exclusive; send one or the other.");
|
|
1532
1670
|
}
|
|
1533
1671
|
if (options.files != null || options.bundle_b64 != null) {
|
|
1534
|
-
throw new
|
|
1672
|
+
throw new RequestArgumentError("method_ref and a method bundle (bundle_b64 / files) are mutually exclusive; send one or the other.");
|
|
1535
1673
|
}
|
|
1536
1674
|
if (nonEmptyString(options.method_id) !== undefined) {
|
|
1537
|
-
throw new
|
|
1675
|
+
throw new RequestArgumentError("method_ref and method_id are mutually exclusive: an address run carries its own provenance " +
|
|
1538
1676
|
"and takes no run-history linkage id. Send exactly one method selector.");
|
|
1539
1677
|
}
|
|
1540
1678
|
}
|
|
@@ -1634,21 +1772,31 @@ const REQUEST_ID_HEADER = "x-request-id";
|
|
|
1634
1772
|
* `Run finished with status <STATUS>` the platform keeps for exactly this reader, so the status
|
|
1635
1773
|
* word is recovered from it then; a `409` that yields no known status either way (the one this
|
|
1636
1774
|
* route answers for a stored result it refuses to read) reads as `FAILED`, and its `detail`
|
|
1637
|
-
* still says what happened. The report is
|
|
1638
|
-
* taken as the report,
|
|
1775
|
+
* still says what happened. The report is checked field by field by `readRunErrorReport`: an
|
|
1776
|
+
* object is taken as the report, each named field kept when it has its declared type, and
|
|
1777
|
+
* anything that is not an object reads as no report.
|
|
1639
1778
|
*/
|
|
1640
1779
|
function runResultFailed(runId, body) {
|
|
1641
1780
|
const { serverMessage, document } = parseErrorBody(body);
|
|
1642
1781
|
const message = serverMessage ?? "Run finished without a result.";
|
|
1643
|
-
const rawReport = document?.error;
|
|
1644
1782
|
return {
|
|
1645
1783
|
state: "failed",
|
|
1646
1784
|
pipeline_run_id: runId,
|
|
1647
1785
|
status: knownRunStatus(document?.run_status) ?? statusFromDetail(message) ?? "FAILED",
|
|
1648
1786
|
message,
|
|
1649
|
-
error:
|
|
1787
|
+
error: readRunErrorReport(document?.error),
|
|
1650
1788
|
};
|
|
1651
1789
|
}
|
|
1790
|
+
/**
|
|
1791
|
+
* A run record with its stored report checked by `readRunErrorReport`. Only when the record
|
|
1792
|
+
* carries the `error` key, so a server that does not serve it still leaves it absent; a record
|
|
1793
|
+
* that is not an object is returned as it came.
|
|
1794
|
+
*/
|
|
1795
|
+
function withCheckedReport(record) {
|
|
1796
|
+
if (!isPlainObject(record) || !Object.hasOwn(record, "error"))
|
|
1797
|
+
return record;
|
|
1798
|
+
return { ...record, error: readRunErrorReport(record.error) };
|
|
1799
|
+
}
|
|
1652
1800
|
function knownRunStatus(value) {
|
|
1653
1801
|
return typeof value === "string" && KNOWN_RUN_STATUSES.includes(value)
|
|
1654
1802
|
? value
|
|
@@ -1671,8 +1819,9 @@ function statusFromDetail(detail) {
|
|
|
1671
1819
|
* on a non-JSON or non-object body.
|
|
1672
1820
|
*
|
|
1673
1821
|
* Each typed member is kept only when it has the type the problem document gives it, so a
|
|
1674
|
-
* malformed member reads as absent rather than as a wrong value
|
|
1675
|
-
*
|
|
1822
|
+
* malformed member reads as absent rather than as a wrong value, and the nested members
|
|
1823
|
+
* (`provider_metadata`, `migration`, `validation_errors`, `errors`) are checked field by field as a
|
|
1824
|
+
* stored report's are; `document` keeps the decoded object whole, members named or not.
|
|
1676
1825
|
*/
|
|
1677
1826
|
function parseErrorBody(body) {
|
|
1678
1827
|
const empty = {
|
|
@@ -1715,10 +1864,10 @@ function parseErrorBody(body) {
|
|
|
1715
1864
|
serverMessage = root.message;
|
|
1716
1865
|
// `validation_errors` rides the problem envelope as a top-level array (the
|
|
1717
1866
|
// VERBOSE projection of `ErrorReport.validation_errors`, retained under STRICT
|
|
1718
|
-
// too — it describes the caller's own bundle, not server internals).
|
|
1719
|
-
//
|
|
1867
|
+
// too — it describes the caller's own bundle, not server internals). Each item is
|
|
1868
|
+
// kept when it is an object with a string `category` and `message`, as on a stored report.
|
|
1720
1869
|
const validationErrors = Array.isArray(root.validation_errors)
|
|
1721
|
-
? root.validation_errors
|
|
1870
|
+
? root.validation_errors.filter(isValidationItem)
|
|
1722
1871
|
: undefined;
|
|
1723
1872
|
// The platform's closed native code (`conflict`, `not_found`, …), one-to-one with `type`.
|
|
1724
1873
|
const code = typeof root.code === "string" ? root.code : undefined;
|
|
@@ -1733,19 +1882,18 @@ function parseErrorBody(body) {
|
|
|
1733
1882
|
userAction: parseUserAction(root.user_action),
|
|
1734
1883
|
model: stringMember(root.model),
|
|
1735
1884
|
provider: stringMember(root.provider),
|
|
1885
|
+
// The nested members are checked one level down by the readers a stored report goes
|
|
1886
|
+
// through, so a misfit field reads as absent here exactly as it does there.
|
|
1736
1887
|
providerMetadata: isPlainObject(root.provider_metadata)
|
|
1737
|
-
? root.provider_metadata
|
|
1888
|
+
? readProviderMetadata(root.provider_metadata)
|
|
1738
1889
|
: undefined,
|
|
1739
|
-
migration: isPlainObject(root.migration) ? root.migration : undefined,
|
|
1890
|
+
migration: isPlainObject(root.migration) ? readMigration(root.migration) : undefined,
|
|
1740
1891
|
errors: Array.isArray(root.errors)
|
|
1741
|
-
? root.errors.filter(isPlainObject)
|
|
1892
|
+
? root.errors.filter(isPlainObject).map(readFieldError)
|
|
1742
1893
|
: undefined,
|
|
1743
1894
|
};
|
|
1744
1895
|
return { errorType, serverMessage, validationErrors, code, problem, document: root };
|
|
1745
1896
|
}
|
|
1746
|
-
function isPlainObject(value) {
|
|
1747
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1748
|
-
}
|
|
1749
1897
|
/** A problem member kept only when it is a non-empty string. */
|
|
1750
1898
|
function stringMember(value) {
|
|
1751
1899
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|