mthds 0.9.0 → 0.10.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 +43 -21
- package/dist/agent/commands/api-commands.js +290 -93
- package/dist/agent/commands/api-commands.js.map +1 -1
- package/dist/agent/commands/config.js +2 -2
- package/dist/agent/commands/config.js.map +1 -1
- package/dist/agent/commands/validate.js +5 -13
- package/dist/agent/commands/validate.js.map +1 -1
- package/dist/agent-cli.js +5 -5
- package/dist/agent-cli.js.map +1 -1
- package/dist/cli/commands/config.js +2 -2
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/install.js +19 -39
- package/dist/cli/commands/install.js.map +1 -1
- package/dist/cli/commands/run.js +82 -69
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/setup.js +22 -23
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/utils.d.ts +1 -1
- package/dist/cli/commands/validate.js +10 -14
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/config/config.d.ts +14 -1
- package/dist/config/config.js +31 -6
- package/dist/config/config.js.map +1 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.js +22 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol/concept.d.ts +14 -0
- package/dist/protocol/concept.js +10 -0
- package/dist/protocol/concept.js.map +1 -0
- package/dist/protocol/exceptions.d.ts +10 -0
- package/dist/protocol/exceptions.js +12 -0
- package/dist/protocol/exceptions.js.map +1 -0
- package/dist/protocol/models.d.ts +95 -0
- package/dist/protocol/models.js +24 -0
- package/dist/protocol/models.js.map +1 -0
- package/dist/protocol/options.d.ts +60 -0
- package/dist/protocol/options.js +11 -0
- package/dist/protocol/options.js.map +1 -0
- package/dist/protocol/pipe_output.d.ts +11 -0
- package/dist/protocol/pipe_output.js +7 -0
- package/dist/protocol/pipe_output.js.map +1 -0
- package/dist/protocol/pipeline_inputs.d.ts +8 -0
- package/dist/protocol/pipeline_inputs.js +7 -0
- package/dist/protocol/pipeline_inputs.js.map +1 -0
- package/dist/protocol/protocol.d.ts +47 -0
- package/dist/{client → protocol}/protocol.js.map +1 -1
- package/dist/protocol/stuff.d.ts +16 -0
- package/dist/protocol/stuff.js +8 -0
- package/dist/{client/models → protocol}/stuff.js.map +1 -1
- package/dist/protocol/working_memory.d.ts +10 -0
- package/dist/protocol/working_memory.js +7 -0
- package/dist/protocol/working_memory.js.map +1 -0
- package/dist/runners/api/client.d.ts +170 -0
- package/dist/runners/api/client.js +653 -0
- package/dist/runners/api/client.js.map +1 -0
- package/dist/runners/api/exceptions.d.ts +106 -0
- package/dist/runners/api/exceptions.js +141 -0
- package/dist/runners/api/exceptions.js.map +1 -0
- package/dist/runners/api/models.d.ts +38 -0
- package/dist/runners/api/models.js +13 -0
- package/dist/runners/api/models.js.map +1 -0
- package/dist/runners/api/runs.d.ts +130 -0
- package/dist/runners/api/runs.js +93 -0
- package/dist/runners/api/runs.js.map +1 -0
- package/dist/runners/base-runner.d.ts +27 -0
- package/dist/runners/base-runner.js +25 -0
- package/dist/runners/base-runner.js.map +1 -0
- package/dist/runners/pipelex/runner.d.ts +38 -0
- package/dist/runners/{pipelex-runner.js → pipelex/runner.js} +168 -83
- package/dist/runners/pipelex/runner.js.map +1 -0
- package/dist/runners/registry.js +10 -4
- package/dist/runners/registry.js.map +1 -1
- package/dist/runners/types.d.ts +13 -71
- package/dist/runners/types.js.map +1 -1
- package/package.json +6 -3
- package/dist/client/client.d.ts +0 -15
- package/dist/client/client.js +0 -127
- package/dist/client/client.js.map +0 -1
- package/dist/client/exceptions.d.ts +0 -46
- package/dist/client/exceptions.js +0 -61
- package/dist/client/exceptions.js.map +0 -1
- package/dist/client/index.d.ts +0 -5
- package/dist/client/index.js +0 -3
- package/dist/client/index.js.map +0 -1
- package/dist/client/models/index.d.ts +0 -4
- package/dist/client/models/index.js +0 -2
- package/dist/client/models/index.js.map +0 -1
- package/dist/client/models/pipe_output.d.ts +0 -2
- package/dist/client/models/pipe_output.js +0 -2
- package/dist/client/models/pipe_output.js.map +0 -1
- package/dist/client/models/pipeline_inputs.d.ts +0 -3
- package/dist/client/models/pipeline_inputs.js +0 -2
- package/dist/client/models/pipeline_inputs.js.map +0 -1
- package/dist/client/models/stuff.d.ts +0 -1
- package/dist/client/models/stuff.js +0 -2
- package/dist/client/models/working_memory.d.ts +0 -1
- package/dist/client/models/working_memory.js +0 -2
- package/dist/client/models/working_memory.js.map +0 -1
- package/dist/client/pipeline.d.ts +0 -36
- package/dist/client/pipeline.js +0 -2
- package/dist/client/pipeline.js.map +0 -1
- package/dist/client/protocol.d.ts +0 -5
- package/dist/runners/api-runner.d.ts +0 -24
- package/dist/runners/api-runner.js +0 -91
- package/dist/runners/api-runner.js.map +0 -1
- package/dist/runners/pipelex-runner.d.ts +0 -30
- package/dist/runners/pipelex-runner.js.map +0 -1
- /package/dist/{client → protocol}/protocol.js +0 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import { BaseRunner } from "../base-runner.js";
|
|
2
|
+
import { Runners } from "../types.js";
|
|
3
|
+
import { ApiResponseError, ApiUnreachableError, PipelineExecuteTimeoutError, PipelineRequestError, RunLifecycleUnavailableError, RunStillRunningError, } from "./exceptions.js";
|
|
4
|
+
import { isValidBaseUrl } from "../../config/config.js";
|
|
5
|
+
/** Hosted default — the SDK composes every endpoint as `{base}/v1/{endpoint}`. */
|
|
6
|
+
export const DEFAULT_API_BASE_URL = "https://api.pipelex.com";
|
|
7
|
+
// The SDK composes every endpoint from one origin (MTHDS_API_URL): `{base}/v1/{endpoint}`.
|
|
8
|
+
// The same paths are served by the hosted MTHDS API (api.pipelex.com) and by a bare
|
|
9
|
+
// pipelex-api runner (localhost:8081) — the protocol surface is identical; only the
|
|
10
|
+
// hosted extensions (e.g. run polling) differ, detectable via GET /v1/version.
|
|
11
|
+
const API_PREFIX = "v1";
|
|
12
|
+
const RUNS = "runs";
|
|
13
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 1_200_000; // 20 min — matches the runner's blocking execute ceiling.
|
|
14
|
+
const POLL_REQUEST_TIMEOUT_MS = 30_000; // single status/result GETs; the hosted gateway caps responses at ~30s.
|
|
15
|
+
const DEFAULT_DEGRADED_RETRY_SECONDS = 5; // matches the platform's `_DEGRADE_RETRY_AFTER_SECONDS`.
|
|
16
|
+
/**
|
|
17
|
+
* `VersionInfo.implementation` of the bare open-source runner (no run store).
|
|
18
|
+
* Anything else — the hosted `pipelex-hosted` first — is assumed to serve the
|
|
19
|
+
* durable run-lifecycle extension; a wrong guess still fails with the clear
|
|
20
|
+
* `RunLifecycleUnavailableError` on the first poll.
|
|
21
|
+
*/
|
|
22
|
+
const BARE_RUNNER_IMPLEMENTATION = "pipelex-api";
|
|
23
|
+
/**
|
|
24
|
+
* Client for any MTHDS runner — and THE API runner (parity D8). One class,
|
|
25
|
+
* two consumers: `pipelex-app` instantiates it directly as a protocol client,
|
|
26
|
+
* the CLI gets it via `createRunner()` as a full `Runner`. `extends BaseRunner
|
|
27
|
+
* implements Runner` so it carries the protocol surface, the Pipelex build
|
|
28
|
+
* extensions, and the lifecycle composites (`waitForResult` /
|
|
29
|
+
* `startAndWaitForResult`, inherited from `BaseRunner`).
|
|
30
|
+
*
|
|
31
|
+
* One base URL (`MTHDS_API_URL`); every endpoint is `<base>/v1/<endpoint>`:
|
|
32
|
+
* - **protocol** (`execute` / `start` / `validate` / `models` / `version`) — works
|
|
33
|
+
* against any MTHDS-compliant runner, hosted or bare.
|
|
34
|
+
* - **run lifecycle** (`getRunStatus` / `getRunResult` / `waitForResult`) — the
|
|
35
|
+
* durable polling extension that survives long runs and lets a caller resume by
|
|
36
|
+
* id. Served only by a deployment that includes the platform block (the hosted
|
|
37
|
+
* MTHDS API); a bare `pipelex-api` runner 404s those routes, which the lifecycle
|
|
38
|
+
* methods translate into a clear `RunLifecycleUnavailableError`.
|
|
39
|
+
*/
|
|
40
|
+
export class MthdsApiClient extends BaseRunner {
|
|
41
|
+
type = Runners.API;
|
|
42
|
+
apiToken;
|
|
43
|
+
baseUrl;
|
|
44
|
+
/** Origin root derived from the base URL — `/health` lives here, not under `/v1`. */
|
|
45
|
+
originUrl;
|
|
46
|
+
/** Cached `/v1/version` handshake outcome — whether the durable lifecycle is served. */
|
|
47
|
+
lifecycleAvailable;
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
super();
|
|
50
|
+
this.apiToken = options.apiToken ?? process.env.MTHDS_API_KEY;
|
|
51
|
+
const normalizedBaseUrl = (options.baseUrl ??
|
|
52
|
+
process.env.MTHDS_API_URL ??
|
|
53
|
+
DEFAULT_API_BASE_URL).replace(/\/+$/, "");
|
|
54
|
+
// `config set base-url` validates host-only; direct SDK usage and
|
|
55
|
+
// MTHDS_API_URL reach this constructor and must be held to the same rule,
|
|
56
|
+
// or a path-prefixed value (e.g. `.../v1`) composes as `/v1/v1/...` and
|
|
57
|
+
// fails with a misleading endpoint error instead of a clear base-URL one.
|
|
58
|
+
// Trailing slashes are stripped first (leniency the SDK has always had);
|
|
59
|
+
// a remaining path/query/fragment/credentials is rejected.
|
|
60
|
+
if (!isValidBaseUrl(normalizedBaseUrl)) {
|
|
61
|
+
throw new PipelineRequestError(`Invalid API base URL "${normalizedBaseUrl}": must be host-only ` +
|
|
62
|
+
`(http/https, no path, query, fragment, or credentials). Endpoints ` +
|
|
63
|
+
`compose as {base}/v1/{endpoint}.`);
|
|
64
|
+
}
|
|
65
|
+
this.baseUrl = normalizedBaseUrl;
|
|
66
|
+
this.originUrl = new URL("/", this.baseUrl).origin;
|
|
67
|
+
}
|
|
68
|
+
// ── URL resolution ───────────────────────────────────────────────────
|
|
69
|
+
/** Build an API URL: `<base>/v1/<endpoint>`. */
|
|
70
|
+
url(endpoint) {
|
|
71
|
+
return `${this.baseUrl}/${API_PREFIX}/${endpoint.replace(/^\/+/, "")}`;
|
|
72
|
+
}
|
|
73
|
+
// ── Transport ──────────────────────────────────────────────────────
|
|
74
|
+
/**
|
|
75
|
+
* Issue one HTTP request and return the raw status/headers/body. Wraps
|
|
76
|
+
* DNS/connect/TLS/timeout failures as `ApiUnreachableError`; a caller-driven
|
|
77
|
+
* abort (Ctrl-C / agent walk-away) propagates as-is so the poll loop can stop
|
|
78
|
+
* cleanly. Non-2xx interpretation is left to the caller. `url` is a fully
|
|
79
|
+
* resolved absolute URL.
|
|
80
|
+
*/
|
|
81
|
+
async requestRaw(method, url, options = {}) {
|
|
82
|
+
const headers = { Accept: "application/json" };
|
|
83
|
+
if (this.apiToken) {
|
|
84
|
+
headers["Authorization"] = `Bearer ${this.apiToken}`;
|
|
85
|
+
}
|
|
86
|
+
const hasBody = options.body !== undefined;
|
|
87
|
+
if (hasBody) {
|
|
88
|
+
headers["Content-Type"] = "application/json";
|
|
89
|
+
}
|
|
90
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timer = setTimeout(() => controller.abort(new DOMException("Request timed out.", "TimeoutError")), timeoutMs);
|
|
93
|
+
const userSignal = options.signal;
|
|
94
|
+
const onUserAbort = () => controller.abort(userSignal?.reason);
|
|
95
|
+
if (userSignal) {
|
|
96
|
+
if (userSignal.aborted)
|
|
97
|
+
controller.abort(userSignal.reason);
|
|
98
|
+
else
|
|
99
|
+
userSignal.addEventListener("abort", onUserAbort, { once: true });
|
|
100
|
+
}
|
|
101
|
+
let response;
|
|
102
|
+
try {
|
|
103
|
+
response = await fetch(url, {
|
|
104
|
+
method,
|
|
105
|
+
headers,
|
|
106
|
+
body: hasBody ? JSON.stringify(options.body) : undefined,
|
|
107
|
+
signal: controller.signal,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
// A caller-initiated abort (not our timeout) propagates untouched so
|
|
112
|
+
// `waitForResult` callers can distinguish "I stopped waiting" from a
|
|
113
|
+
// network failure.
|
|
114
|
+
if (userSignal?.aborted)
|
|
115
|
+
throw err;
|
|
116
|
+
// undici (Node fetch) wraps DNS/connect/TLS failures as
|
|
117
|
+
// `TypeError("fetch failed")` with the system error attached as `cause`.
|
|
118
|
+
// Our timeout aborts the controller with a "TimeoutError" DOMException.
|
|
119
|
+
const code = extractNetworkErrorCode(err);
|
|
120
|
+
throw new ApiUnreachableError(`Could not reach MTHDS API at ${this.baseUrl} (${code ?? "network error"})`, this.baseUrl, code, { cause: err });
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
if (userSignal)
|
|
125
|
+
userSignal.removeEventListener("abort", onUserAbort);
|
|
126
|
+
}
|
|
127
|
+
const body = await response.text().catch(() => "");
|
|
128
|
+
return {
|
|
129
|
+
status: response.status,
|
|
130
|
+
statusText: response.statusText,
|
|
131
|
+
headers: response.headers,
|
|
132
|
+
body,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Issue a request and parse the JSON body, throwing a plain `Error` on a
|
|
137
|
+
* non-2xx response. Used by the build extensions and `health` — surfaces
|
|
138
|
+
* that don't need the protocol's structured error taxonomy.
|
|
139
|
+
*/
|
|
140
|
+
async requestJson(method, url, body) {
|
|
141
|
+
const headers = { Accept: "application/json" };
|
|
142
|
+
if (this.apiToken) {
|
|
143
|
+
headers["Authorization"] = `Bearer ${this.apiToken}`;
|
|
144
|
+
}
|
|
145
|
+
if (body !== undefined) {
|
|
146
|
+
headers["Content-Type"] = "application/json";
|
|
147
|
+
}
|
|
148
|
+
const res = await fetch(url, {
|
|
149
|
+
method,
|
|
150
|
+
headers,
|
|
151
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
152
|
+
});
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
const text = await res.text().catch(() => "");
|
|
155
|
+
throw new Error(`API ${method} ${url} failed (${res.status}): ${text || res.statusText}`);
|
|
156
|
+
}
|
|
157
|
+
return res.json();
|
|
158
|
+
}
|
|
159
|
+
postApi(path, body) {
|
|
160
|
+
return this.requestJson("POST", this.url(path), body);
|
|
161
|
+
}
|
|
162
|
+
throwApiResponseError(method, endpoint, res) {
|
|
163
|
+
const { errorType, serverMessage } = parseErrorBody(res.body);
|
|
164
|
+
throw new ApiResponseError(`API ${method} /${API_PREFIX}/${endpoint} failed (${res.status}): ${serverMessage ?? (res.body || res.statusText)}`, this.baseUrl, res.status, res.statusText, res.body, errorType, serverMessage);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Translate a "route absent" 404 (a bare pipelex-api with no platform block)
|
|
168
|
+
* into a clear `RunLifecycleUnavailableError`. The platform's own 404s (run
|
|
169
|
+
* not found / cross-org) carry a structured error envelope (a `code` field)
|
|
170
|
+
* and are left for normal handling.
|
|
171
|
+
*/
|
|
172
|
+
throwIfLifecycleUnavailable(res, url) {
|
|
173
|
+
if (res.status !== 404)
|
|
174
|
+
return;
|
|
175
|
+
if (!isMissingRoute404(res.body))
|
|
176
|
+
return;
|
|
177
|
+
throw new RunLifecycleUnavailableError(`The durable run lifecycle is not available: ${url} returned 404. Run polling is a ` +
|
|
178
|
+
`hosted-API extension (/${API_PREFIX}/${RUNS}/*), not part of the MTHDS Protocol; ` +
|
|
179
|
+
"MTHDS_API_URL points at a bare runner that does not serve it.", this.baseUrl);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Map the protocol's optional 202 execute degrade to a typed
|
|
183
|
+
* error. Hosted does not emit 202 today, but the protocol permits it;
|
|
184
|
+
* raising a typed error (with the `pipeline_run_id` + `Location` + `Retry-After`
|
|
185
|
+
* hints) beats a generic parse failure on an unexpected body shape.
|
|
186
|
+
*/
|
|
187
|
+
throwIfExecuteDegraded(res) {
|
|
188
|
+
if (res.status !== 202)
|
|
189
|
+
return;
|
|
190
|
+
let runId = "";
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(res.body);
|
|
193
|
+
if (parsed && typeof parsed === "object") {
|
|
194
|
+
const candidate = parsed.pipeline_run_id;
|
|
195
|
+
if (typeof candidate === "string")
|
|
196
|
+
runId = candidate;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Non-JSON 202 body — keep runId empty; the error message covers it.
|
|
201
|
+
}
|
|
202
|
+
throw new RunStillRunningError(`execute() was accepted asynchronously (202): run ${runId || "<unknown>"} is still ` +
|
|
203
|
+
"running server-side. Poll its results (hosted) or use start().", runId, parseRetryAfter(res.headers), res.headers.get("location"));
|
|
204
|
+
}
|
|
205
|
+
// ── Health ────────────────────────────────────────────────────────
|
|
206
|
+
async health() {
|
|
207
|
+
// `/health` is origin-level, NOT under the `/v1` prefix.
|
|
208
|
+
return this.requestJson("GET", `${this.originUrl}/health`);
|
|
209
|
+
}
|
|
210
|
+
// ── Protocol surface ─────────────────────────────────────────────────
|
|
211
|
+
/**
|
|
212
|
+
* Execute a method synchronously and wait for its completion —
|
|
213
|
+
* `POST /v1/execute`.
|
|
214
|
+
*
|
|
215
|
+
* Behind the hosted gateway, synchronous requests terminate at ~30s; a run
|
|
216
|
+
* that exceeds that surfaces as `PipelineExecuteTimeoutError` pointing at the
|
|
217
|
+
* durable start+poll path. Throws `RunStillRunningError` on the protocol's
|
|
218
|
+
* optional 202 degrade.
|
|
219
|
+
*/
|
|
220
|
+
async execute(options) {
|
|
221
|
+
const extensions = buildExtensions(options.extra);
|
|
222
|
+
if (!options.pipe_code &&
|
|
223
|
+
(!options.mthds_contents || options.mthds_contents.length === 0) &&
|
|
224
|
+
Object.keys(extensions).length === 0) {
|
|
225
|
+
throw new PipelineRequestError("Either pipe_code, mthds_contents or a server-specific extension arg (extra) must be provided to execute().");
|
|
226
|
+
}
|
|
227
|
+
const request = {
|
|
228
|
+
pipe_code: options.pipe_code,
|
|
229
|
+
mthds_contents: options.mthds_contents,
|
|
230
|
+
inputs: options.inputs,
|
|
231
|
+
output_name: options.output_name,
|
|
232
|
+
output_multiplicity: options.output_multiplicity,
|
|
233
|
+
dynamic_output_concept_ref: options.dynamic_output_concept_ref,
|
|
234
|
+
...extensions,
|
|
235
|
+
};
|
|
236
|
+
const startedAt = Date.now();
|
|
237
|
+
try {
|
|
238
|
+
const res = await this.requestRaw("POST", this.url("execute"), {
|
|
239
|
+
body: request,
|
|
240
|
+
});
|
|
241
|
+
this.throwIfExecuteDegraded(res);
|
|
242
|
+
if (res.status < 200 || res.status >= 300) {
|
|
243
|
+
this.throwApiResponseError("POST", "execute", res);
|
|
244
|
+
}
|
|
245
|
+
return JSON.parse(res.body);
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
if (err instanceof RunStillRunningError)
|
|
249
|
+
throw err;
|
|
250
|
+
// The hosted gateway terminates synchronous requests at ~30s. A run that
|
|
251
|
+
// exceeds that comes back as a gateway 503/504 (or a client abort) —
|
|
252
|
+
// translate it into a clear, actionable error pointing at start+poll.
|
|
253
|
+
const elapsedMs = Date.now() - startedAt;
|
|
254
|
+
if (isGatewayTimeout(err, elapsedMs)) {
|
|
255
|
+
throw new PipelineExecuteTimeoutError(elapsedMs, { cause: err });
|
|
256
|
+
}
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Start a method asynchronously — `POST /v1/start` (202, no output yet).
|
|
262
|
+
*
|
|
263
|
+
* Server-specific extension args ride `options.extra` and merge into the
|
|
264
|
+
* request body — the server you call defines and handles them (including a
|
|
265
|
+
* client-supplied run id where a server supports one). The returned
|
|
266
|
+
* `pipeline_run_id` is always authoritative; on a hosted deployment it is
|
|
267
|
+
* durable — poll `getRunStatus` / `getRunResult`.
|
|
268
|
+
*/
|
|
269
|
+
async start(options) {
|
|
270
|
+
const extensions = buildExtensions(options.extra);
|
|
271
|
+
if (!options.pipe_code &&
|
|
272
|
+
(!options.mthds_contents || options.mthds_contents.length === 0) &&
|
|
273
|
+
Object.keys(extensions).length === 0) {
|
|
274
|
+
throw new PipelineRequestError("Either pipe_code, mthds_contents or a server-specific extension arg (extra) must be provided to start().");
|
|
275
|
+
}
|
|
276
|
+
// `?? undefined` so JSON.stringify drops absent fields from the wire body.
|
|
277
|
+
const request = {
|
|
278
|
+
pipe_code: options.pipe_code ?? undefined,
|
|
279
|
+
mthds_contents: options.mthds_contents ?? undefined,
|
|
280
|
+
inputs: options.inputs ?? undefined,
|
|
281
|
+
output_name: options.output_name ?? undefined,
|
|
282
|
+
output_multiplicity: options.output_multiplicity ?? undefined,
|
|
283
|
+
dynamic_output_concept_ref: options.dynamic_output_concept_ref ?? undefined,
|
|
284
|
+
...extensions,
|
|
285
|
+
};
|
|
286
|
+
const url = this.url("start");
|
|
287
|
+
const res = await this.requestRaw("POST", url, {
|
|
288
|
+
body: request,
|
|
289
|
+
timeoutMs: POLL_REQUEST_TIMEOUT_MS,
|
|
290
|
+
});
|
|
291
|
+
// A bare runner with no run store 404s here just as it does on the result
|
|
292
|
+
// routes — surface the same clear `RunLifecycleUnavailableError` (and let
|
|
293
|
+
// `startAndWaitForResult` fall back to the blocking `execute`).
|
|
294
|
+
this.throwIfLifecycleUnavailable(res, url);
|
|
295
|
+
if (res.status < 200 || res.status >= 300) {
|
|
296
|
+
this.throwApiResponseError("POST", "start", res);
|
|
297
|
+
}
|
|
298
|
+
return JSON.parse(res.body);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Parse, validate, and dry-run an MTHDS bundle — `POST /v1/validate`.
|
|
302
|
+
*
|
|
303
|
+
* Returns the structural artifacts of a valid bundle; an invalid bundle is
|
|
304
|
+
* an HTTP 422 problem, surfaced as `ApiResponseError`.
|
|
305
|
+
*/
|
|
306
|
+
async validate(mthdsContents, allowSignatures = false) {
|
|
307
|
+
const res = await this.requestRaw("POST", this.url("validate"), {
|
|
308
|
+
body: { mthds_contents: mthdsContents, allow_signatures: allowSignatures },
|
|
309
|
+
});
|
|
310
|
+
if (res.status < 200 || res.status >= 300) {
|
|
311
|
+
this.throwApiResponseError("POST", "validate", res);
|
|
312
|
+
}
|
|
313
|
+
return JSON.parse(res.body);
|
|
314
|
+
}
|
|
315
|
+
/** The model deck the runner can route to — `GET /v1/models[?type=]`. */
|
|
316
|
+
async models(category) {
|
|
317
|
+
const endpoint = category
|
|
318
|
+
? `models?type=${encodeURIComponent(category)}`
|
|
319
|
+
: "models";
|
|
320
|
+
const res = await this.requestRaw("GET", this.url(endpoint), {
|
|
321
|
+
timeoutMs: POLL_REQUEST_TIMEOUT_MS,
|
|
322
|
+
});
|
|
323
|
+
if (res.status < 200 || res.status >= 300) {
|
|
324
|
+
this.throwApiResponseError("GET", endpoint, res);
|
|
325
|
+
}
|
|
326
|
+
return JSON.parse(res.body);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Protocol + implementation versions — `GET /v1/version` (always public).
|
|
330
|
+
* The handshake for feature detection (hosted extensions or not).
|
|
331
|
+
*/
|
|
332
|
+
async version() {
|
|
333
|
+
const res = await this.requestRaw("GET", this.url("version"), {
|
|
334
|
+
timeoutMs: POLL_REQUEST_TIMEOUT_MS,
|
|
335
|
+
});
|
|
336
|
+
if (res.status < 200 || res.status >= 300) {
|
|
337
|
+
this.throwApiResponseError("GET", "version", res);
|
|
338
|
+
}
|
|
339
|
+
return JSON.parse(res.body);
|
|
340
|
+
}
|
|
341
|
+
// ── Build extensions (Pipelex API layer 2 — `/v1/build/*`) ────────
|
|
342
|
+
async buildInputs(request) {
|
|
343
|
+
return this.postApi("build/inputs", request);
|
|
344
|
+
}
|
|
345
|
+
async buildOutput(request) {
|
|
346
|
+
return this.postApi("build/output", request);
|
|
347
|
+
}
|
|
348
|
+
async buildRunner(request) {
|
|
349
|
+
return this.postApi("build/runner", request);
|
|
350
|
+
}
|
|
351
|
+
async concept(request) {
|
|
352
|
+
return this.postApi("build/concept", request);
|
|
353
|
+
}
|
|
354
|
+
async pipeSpec(request) {
|
|
355
|
+
return this.postApi("build/pipe-spec", request);
|
|
356
|
+
}
|
|
357
|
+
// ── Hosted extension: durable run lifecycle (NOT part of the protocol) ──
|
|
358
|
+
/**
|
|
359
|
+
* Fetch a run's status by bare id — `GET /v1/runs/{pipeline_run_id}/status`.
|
|
360
|
+
*
|
|
361
|
+
* Self-healing: a finished-but-unrecorded run resolves to its true terminal
|
|
362
|
+
* status on read. `degraded: true` means Temporal was unreachable and
|
|
363
|
+
* `status` is the last-known value; `retry_after_seconds` carries the
|
|
364
|
+
* server's backoff hint when present. Throws `RunLifecycleUnavailableError`
|
|
365
|
+
* when the lifecycle routes are absent (a bare runner).
|
|
366
|
+
*/
|
|
367
|
+
async getRunStatus(runId, options = {}) {
|
|
368
|
+
const endpoint = `${RUNS}/${encodeURIComponent(runId)}/status`;
|
|
369
|
+
const url = this.url(endpoint);
|
|
370
|
+
const res = await this.requestRaw("GET", url, {
|
|
371
|
+
timeoutMs: POLL_REQUEST_TIMEOUT_MS,
|
|
372
|
+
signal: options.signal,
|
|
373
|
+
});
|
|
374
|
+
this.throwIfLifecycleUnavailable(res, url);
|
|
375
|
+
if (res.status < 200 || res.status >= 300) {
|
|
376
|
+
this.throwApiResponseError("GET", endpoint, res);
|
|
377
|
+
}
|
|
378
|
+
const run = JSON.parse(res.body);
|
|
379
|
+
const retryAfter = parseRetryAfter(res.headers);
|
|
380
|
+
return retryAfter !== null ? { ...run, retry_after_seconds: retryAfter } : run;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Single-shot result lookup — `GET /v1/runs/{pipeline_run_id}/results`.
|
|
384
|
+
* Maps the server's poll semantics to a discriminated union:
|
|
385
|
+
* - HTTP 202 → `running` (with the `Retry-After` hint)
|
|
386
|
+
* - HTTP 200 → `completed` (with the result artifacts)
|
|
387
|
+
* - HTTP 409 → `failed` (terminal non-`COMPLETED`)
|
|
388
|
+
* - HTTP 503 → `running` (Temporal degraded — retry, never fail a poller)
|
|
389
|
+
*
|
|
390
|
+
* Throws `RunLifecycleUnavailableError` when the lifecycle routes are absent
|
|
391
|
+
* (a bare runner).
|
|
392
|
+
*/
|
|
393
|
+
async getRunResult(runId, options = {}) {
|
|
394
|
+
const endpoint = `${RUNS}/${encodeURIComponent(runId)}/results`;
|
|
395
|
+
const url = this.url(endpoint);
|
|
396
|
+
const res = await this.requestRaw("GET", url, {
|
|
397
|
+
timeoutMs: POLL_REQUEST_TIMEOUT_MS,
|
|
398
|
+
signal: options.signal,
|
|
399
|
+
});
|
|
400
|
+
if (res.status === 202 || res.status === 503) {
|
|
401
|
+
return {
|
|
402
|
+
state: "running",
|
|
403
|
+
pipeline_run_id: runId,
|
|
404
|
+
retry_after_seconds: parseRetryAfter(res.headers) ?? DEFAULT_DEGRADED_RETRY_SECONDS,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (res.status === 409) {
|
|
408
|
+
const { serverMessage } = parseErrorBody(res.body);
|
|
409
|
+
const message = serverMessage ?? "Run finished without a result.";
|
|
410
|
+
return {
|
|
411
|
+
state: "failed",
|
|
412
|
+
pipeline_run_id: runId,
|
|
413
|
+
status: extractRunStatusFromMessage(message),
|
|
414
|
+
message,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
this.throwIfLifecycleUnavailable(res, url);
|
|
418
|
+
if (res.status < 200 || res.status >= 300) {
|
|
419
|
+
this.throwApiResponseError("GET", endpoint, res);
|
|
420
|
+
}
|
|
421
|
+
const result = JSON.parse(res.body);
|
|
422
|
+
return { state: "completed", pipeline_run_id: runId, result };
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Whether the configured server serves the durable run lifecycle, decided
|
|
426
|
+
* via the `GET /v1/version` handshake (master D2) and cached for the
|
|
427
|
+
* client's lifetime. A bare `pipelex-api` runner has no run store; anything
|
|
428
|
+
* else is assumed hosted. When the handshake itself fails, assume hosted
|
|
429
|
+
* (the SDK default) and let the start call surface the real error.
|
|
430
|
+
*/
|
|
431
|
+
async supportsRunLifecycle() {
|
|
432
|
+
if (this.lifecycleAvailable === undefined) {
|
|
433
|
+
try {
|
|
434
|
+
const info = await this.version();
|
|
435
|
+
const impl = info.implementation;
|
|
436
|
+
this.lifecycleAvailable = !(typeof impl === "string" && impl === BARE_RUNNER_IMPLEMENTATION);
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
this.lifecycleAvailable = true;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return this.lifecycleAvailable;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Start a run and wait for its result.
|
|
446
|
+
*
|
|
447
|
+
* - **Hosted** (per the `/v1/version` handshake): durable start + poll (the
|
|
448
|
+
* `BaseRunner` composite), the path that survives the gateway's ~30s
|
|
449
|
+
* synchronous ceiling.
|
|
450
|
+
* - **Bare runner** (no run store): the blocking `POST /v1/execute`, which
|
|
451
|
+
* has no gateway cap off-platform and returns the native `pipe_output`.
|
|
452
|
+
*/
|
|
453
|
+
async startAndWaitForResult(options, pollOptions) {
|
|
454
|
+
if (await this.supportsRunLifecycle()) {
|
|
455
|
+
// A runner can look hosted yet lack the durable routes — `implementation`
|
|
456
|
+
// is an extension field, so a compliant bare runner that omits it is
|
|
457
|
+
// misdetected here. Such a runner raises `RunLifecycleUnavailableError`
|
|
458
|
+
// from `start()`, BEFORE any run is created, so falling back to the
|
|
459
|
+
// blocking path cannot double-run. Cache the negative so later calls skip
|
|
460
|
+
// the durable attempt.
|
|
461
|
+
let ack;
|
|
462
|
+
try {
|
|
463
|
+
ack = await this.start(options);
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
if (!(err instanceof RunLifecycleUnavailableError))
|
|
467
|
+
throw err;
|
|
468
|
+
this.lifecycleAvailable = false;
|
|
469
|
+
return this.executeBlocking(options);
|
|
470
|
+
}
|
|
471
|
+
return this.waitForResult(ack.pipeline_run_id, pollOptions);
|
|
472
|
+
}
|
|
473
|
+
return this.executeBlocking(options);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Blocking `POST /v1/execute` adapted onto `RunResults` — the bare-runner
|
|
477
|
+
* path. Forwards every protocol field PLUS the `extra` extension passthrough:
|
|
478
|
+
* an extension-only call (`{ extra }` with no pipe_code/bundle) or a vendor
|
|
479
|
+
* selector riding `extra` must survive this path, not just the durable one.
|
|
480
|
+
*/
|
|
481
|
+
async executeBlocking(options) {
|
|
482
|
+
const response = await this.execute({
|
|
483
|
+
pipe_code: options.pipe_code ?? undefined,
|
|
484
|
+
mthds_contents: options.mthds_contents ?? undefined,
|
|
485
|
+
inputs: options.inputs ?? undefined,
|
|
486
|
+
output_name: options.output_name ?? undefined,
|
|
487
|
+
output_multiplicity: options.output_multiplicity ?? undefined,
|
|
488
|
+
dynamic_output_concept_ref: options.dynamic_output_concept_ref ?? undefined,
|
|
489
|
+
extra: options.extra ?? undefined,
|
|
490
|
+
});
|
|
491
|
+
return mapRunResultToRunResults(response);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// ── Module helpers ────────────────────────────────────────────────────
|
|
495
|
+
/**
|
|
496
|
+
* Map the protocol's blocking `POST /v1/execute` response onto the lifecycle's
|
|
497
|
+
* `RunResults`. The bare-runner path returns `pipe_output` (native runner
|
|
498
|
+
* shape); `main_stuff` is a hosted-durable artifact and stays null here.
|
|
499
|
+
* Consumers read `main_stuff ?? pipe_output` (the documented hosted/bare
|
|
500
|
+
* output-shape difference).
|
|
501
|
+
*/
|
|
502
|
+
function mapRunResultToRunResults(response) {
|
|
503
|
+
const pipeOutput = response.pipe_output;
|
|
504
|
+
return {
|
|
505
|
+
pipeline_run_id: response.pipeline_run_id,
|
|
506
|
+
main_stuff: null,
|
|
507
|
+
// The bare-runner blocking `pipe_output` carries no graph artifact; the
|
|
508
|
+
// hosted graph_spec rides the durable `/v1/runs/{id}/results` payload.
|
|
509
|
+
graph_spec: null,
|
|
510
|
+
pipe_output: pipeOutput ?? null,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
// The protocol's own request fields — `extra` is for extension args only.
|
|
514
|
+
const PROTOCOL_REQUEST_KEYS = new Set([
|
|
515
|
+
"pipe_code",
|
|
516
|
+
"mthds_contents",
|
|
517
|
+
"inputs",
|
|
518
|
+
"output_name",
|
|
519
|
+
"output_multiplicity",
|
|
520
|
+
"dynamic_output_concept_ref",
|
|
521
|
+
]);
|
|
522
|
+
/**
|
|
523
|
+
* Validate and copy the generic `extra` passthrough. Extension args ride the
|
|
524
|
+
* request body as top-level properties; protocol args must be passed as named
|
|
525
|
+
* options, never smuggled through `extra`.
|
|
526
|
+
*/
|
|
527
|
+
function buildExtensions(extra) {
|
|
528
|
+
if (!extra)
|
|
529
|
+
return {};
|
|
530
|
+
const overlap = Object.keys(extra).filter((key) => PROTOCOL_REQUEST_KEYS.has(key));
|
|
531
|
+
if (overlap.length > 0) {
|
|
532
|
+
throw new PipelineRequestError(`extra carries protocol args [${overlap.sort().join(", ")}] — pass them as named options instead.`);
|
|
533
|
+
}
|
|
534
|
+
return { ...extra };
|
|
535
|
+
}
|
|
536
|
+
// The hosted gateway caps synchronous requests at 30s. A failure at/after this
|
|
537
|
+
// threshold on the blocking execute is the timeout, not a transient outage —
|
|
538
|
+
// the threshold guards against mislabelling a fast 503 (runner genuinely down)
|
|
539
|
+
// as a timeout.
|
|
540
|
+
const GATEWAY_TIMEOUT_THRESHOLD_MS = 28_000;
|
|
541
|
+
function isGatewayTimeout(err, elapsedMs) {
|
|
542
|
+
if (elapsedMs < GATEWAY_TIMEOUT_THRESHOLD_MS)
|
|
543
|
+
return false;
|
|
544
|
+
if (err instanceof ApiResponseError)
|
|
545
|
+
return err.status === 503 || err.status === 504;
|
|
546
|
+
if (err instanceof ApiUnreachableError)
|
|
547
|
+
return err.code === "ABORT_TIMEOUT";
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
function extractNetworkErrorCode(err) {
|
|
551
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
552
|
+
return "ABORT_TIMEOUT";
|
|
553
|
+
}
|
|
554
|
+
if (err instanceof Error) {
|
|
555
|
+
const cause = err.cause;
|
|
556
|
+
if (cause && typeof cause === "object" && "code" in cause) {
|
|
557
|
+
const code = cause.code;
|
|
558
|
+
if (typeof code === "string")
|
|
559
|
+
return code;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Whether a 404 is an unmatched-route 404 (no platform deployed) rather than
|
|
566
|
+
* the platform's structured run-not-found 404. The platform wraps its 404s in
|
|
567
|
+
* a structured envelope with a stable `code`; a bare runner returns
|
|
568
|
+
* Starlette's default `{"detail": "Not Found"}` (no `code`).
|
|
569
|
+
*/
|
|
570
|
+
function isMissingRoute404(body) {
|
|
571
|
+
if (!body)
|
|
572
|
+
return true;
|
|
573
|
+
let parsed;
|
|
574
|
+
try {
|
|
575
|
+
parsed = JSON.parse(body);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
581
|
+
return true;
|
|
582
|
+
return !("code" in parsed);
|
|
583
|
+
}
|
|
584
|
+
/** Parse the `Retry-After` header (seconds form, which the platform uses). */
|
|
585
|
+
function parseRetryAfter(headers) {
|
|
586
|
+
const raw = headers.get("retry-after");
|
|
587
|
+
if (!raw)
|
|
588
|
+
return null;
|
|
589
|
+
const seconds = Number(raw);
|
|
590
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
|
|
591
|
+
}
|
|
592
|
+
const KNOWN_RUN_STATUSES = [
|
|
593
|
+
"PENDING",
|
|
594
|
+
"STARTED",
|
|
595
|
+
"RUNNING",
|
|
596
|
+
"COMPLETED",
|
|
597
|
+
"FAILED",
|
|
598
|
+
"CANCELLED",
|
|
599
|
+
"TERMINATED",
|
|
600
|
+
"TIMED_OUT",
|
|
601
|
+
];
|
|
602
|
+
/**
|
|
603
|
+
* The 409 detail reads "Run finished with status FAILED; no result available".
|
|
604
|
+
* Pull the status word out; default to FAILED if the shape ever changes.
|
|
605
|
+
*/
|
|
606
|
+
function extractRunStatusFromMessage(message) {
|
|
607
|
+
const match = message.match(/status\s+([A-Z_]+)/);
|
|
608
|
+
const candidate = match?.[1];
|
|
609
|
+
if (candidate && KNOWN_RUN_STATUSES.includes(candidate)) {
|
|
610
|
+
return candidate;
|
|
611
|
+
}
|
|
612
|
+
return "FAILED";
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* The API serializes errors as `{"detail": {"error_type": ..., "message": ...}}`
|
|
616
|
+
* (HTTPException with dict detail) or `{"detail": "..."}` (auth 401s and RFC
|
|
617
|
+
* 7807 problems). Both shapes are extracted here. Falls through silently on
|
|
618
|
+
* non-JSON bodies.
|
|
619
|
+
*/
|
|
620
|
+
function parseErrorBody(body) {
|
|
621
|
+
if (!body)
|
|
622
|
+
return { errorType: undefined, serverMessage: undefined };
|
|
623
|
+
let parsed;
|
|
624
|
+
try {
|
|
625
|
+
parsed = JSON.parse(body);
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
return { errorType: undefined, serverMessage: undefined };
|
|
629
|
+
}
|
|
630
|
+
if (!parsed || typeof parsed !== "object") {
|
|
631
|
+
return { errorType: undefined, serverMessage: undefined };
|
|
632
|
+
}
|
|
633
|
+
const root = parsed;
|
|
634
|
+
const detail = root.detail;
|
|
635
|
+
let errorType;
|
|
636
|
+
let serverMessage;
|
|
637
|
+
if (detail && typeof detail === "object") {
|
|
638
|
+
const d = detail;
|
|
639
|
+
if (typeof d.error_type === "string")
|
|
640
|
+
errorType = d.error_type;
|
|
641
|
+
if (typeof d.message === "string")
|
|
642
|
+
serverMessage = d.message;
|
|
643
|
+
}
|
|
644
|
+
else if (typeof detail === "string") {
|
|
645
|
+
serverMessage = detail;
|
|
646
|
+
}
|
|
647
|
+
if (errorType === undefined && typeof root.error_type === "string")
|
|
648
|
+
errorType = root.error_type;
|
|
649
|
+
if (serverMessage === undefined && typeof root.message === "string")
|
|
650
|
+
serverMessage = root.message;
|
|
651
|
+
return { errorType, serverMessage };
|
|
652
|
+
}
|
|
653
|
+
//# sourceMappingURL=client.js.map
|