mthds 0.8.1 → 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.
Files changed (121) hide show
  1. package/README.md +43 -21
  2. package/dist/agent/commands/api-commands.js +290 -93
  3. package/dist/agent/commands/api-commands.js.map +1 -1
  4. package/dist/agent/commands/config.js +2 -2
  5. package/dist/agent/commands/config.js.map +1 -1
  6. package/dist/agent/commands/update-check.d.ts +14 -1
  7. package/dist/agent/commands/update-check.js +238 -16
  8. package/dist/agent/commands/update-check.js.map +1 -1
  9. package/dist/agent/commands/validate.js +5 -13
  10. package/dist/agent/commands/validate.js.map +1 -1
  11. package/dist/agent/plugin-version.d.ts +1 -1
  12. package/dist/agent/plugin-version.js +1 -1
  13. package/dist/agent/remote-version.d.ts +31 -0
  14. package/dist/agent/remote-version.js +74 -0
  15. package/dist/agent/remote-version.js.map +1 -0
  16. package/dist/agent/update-cache.d.ts +41 -0
  17. package/dist/agent/update-cache.js +129 -0
  18. package/dist/agent/update-cache.js.map +1 -1
  19. package/dist/agent-cli.js +5 -5
  20. package/dist/agent-cli.js.map +1 -1
  21. package/dist/cli/commands/config.js +2 -2
  22. package/dist/cli/commands/config.js.map +1 -1
  23. package/dist/cli/commands/install.js +19 -39
  24. package/dist/cli/commands/install.js.map +1 -1
  25. package/dist/cli/commands/run.js +82 -69
  26. package/dist/cli/commands/run.js.map +1 -1
  27. package/dist/cli/commands/setup.js +22 -23
  28. package/dist/cli/commands/setup.js.map +1 -1
  29. package/dist/cli/commands/utils.d.ts +1 -1
  30. package/dist/cli/commands/validate.js +10 -14
  31. package/dist/cli/commands/validate.js.map +1 -1
  32. package/dist/cli.js +2 -2
  33. package/dist/cli.js.map +1 -1
  34. package/dist/config/config.d.ts +14 -1
  35. package/dist/config/config.js +31 -6
  36. package/dist/config/config.js.map +1 -1
  37. package/dist/index.d.ts +27 -1
  38. package/dist/index.js +22 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/protocol/concept.d.ts +14 -0
  41. package/dist/protocol/concept.js +10 -0
  42. package/dist/protocol/concept.js.map +1 -0
  43. package/dist/protocol/exceptions.d.ts +10 -0
  44. package/dist/protocol/exceptions.js +12 -0
  45. package/dist/protocol/exceptions.js.map +1 -0
  46. package/dist/protocol/models.d.ts +95 -0
  47. package/dist/protocol/models.js +24 -0
  48. package/dist/protocol/models.js.map +1 -0
  49. package/dist/protocol/options.d.ts +60 -0
  50. package/dist/protocol/options.js +11 -0
  51. package/dist/protocol/options.js.map +1 -0
  52. package/dist/protocol/pipe_output.d.ts +11 -0
  53. package/dist/protocol/pipe_output.js +7 -0
  54. package/dist/protocol/pipe_output.js.map +1 -0
  55. package/dist/protocol/pipeline_inputs.d.ts +8 -0
  56. package/dist/protocol/pipeline_inputs.js +7 -0
  57. package/dist/protocol/pipeline_inputs.js.map +1 -0
  58. package/dist/protocol/protocol.d.ts +47 -0
  59. package/dist/{client → protocol}/protocol.js.map +1 -1
  60. package/dist/protocol/stuff.d.ts +16 -0
  61. package/dist/protocol/stuff.js +8 -0
  62. package/dist/{client/models → protocol}/stuff.js.map +1 -1
  63. package/dist/protocol/working_memory.d.ts +10 -0
  64. package/dist/protocol/working_memory.js +7 -0
  65. package/dist/protocol/working_memory.js.map +1 -0
  66. package/dist/runners/api/client.d.ts +170 -0
  67. package/dist/runners/api/client.js +653 -0
  68. package/dist/runners/api/client.js.map +1 -0
  69. package/dist/runners/api/exceptions.d.ts +106 -0
  70. package/dist/runners/api/exceptions.js +141 -0
  71. package/dist/runners/api/exceptions.js.map +1 -0
  72. package/dist/runners/api/models.d.ts +38 -0
  73. package/dist/runners/api/models.js +13 -0
  74. package/dist/runners/api/models.js.map +1 -0
  75. package/dist/runners/api/runs.d.ts +130 -0
  76. package/dist/runners/api/runs.js +93 -0
  77. package/dist/runners/api/runs.js.map +1 -0
  78. package/dist/runners/base-runner.d.ts +27 -0
  79. package/dist/runners/base-runner.js +25 -0
  80. package/dist/runners/base-runner.js.map +1 -0
  81. package/dist/runners/pipelex/runner.d.ts +38 -0
  82. package/dist/runners/{pipelex-runner.js → pipelex/runner.js} +168 -83
  83. package/dist/runners/pipelex/runner.js.map +1 -0
  84. package/dist/runners/registry.js +10 -4
  85. package/dist/runners/registry.js.map +1 -1
  86. package/dist/runners/types.d.ts +13 -71
  87. package/dist/runners/types.js.map +1 -1
  88. package/package.json +6 -3
  89. package/dist/client/client.d.ts +0 -15
  90. package/dist/client/client.js +0 -127
  91. package/dist/client/client.js.map +0 -1
  92. package/dist/client/exceptions.d.ts +0 -46
  93. package/dist/client/exceptions.js +0 -61
  94. package/dist/client/exceptions.js.map +0 -1
  95. package/dist/client/index.d.ts +0 -5
  96. package/dist/client/index.js +0 -3
  97. package/dist/client/index.js.map +0 -1
  98. package/dist/client/models/index.d.ts +0 -4
  99. package/dist/client/models/index.js +0 -2
  100. package/dist/client/models/index.js.map +0 -1
  101. package/dist/client/models/pipe_output.d.ts +0 -2
  102. package/dist/client/models/pipe_output.js +0 -2
  103. package/dist/client/models/pipe_output.js.map +0 -1
  104. package/dist/client/models/pipeline_inputs.d.ts +0 -3
  105. package/dist/client/models/pipeline_inputs.js +0 -2
  106. package/dist/client/models/pipeline_inputs.js.map +0 -1
  107. package/dist/client/models/stuff.d.ts +0 -1
  108. package/dist/client/models/stuff.js +0 -2
  109. package/dist/client/models/working_memory.d.ts +0 -1
  110. package/dist/client/models/working_memory.js +0 -2
  111. package/dist/client/models/working_memory.js.map +0 -1
  112. package/dist/client/pipeline.d.ts +0 -36
  113. package/dist/client/pipeline.js +0 -2
  114. package/dist/client/pipeline.js.map +0 -1
  115. package/dist/client/protocol.d.ts +0 -5
  116. package/dist/runners/api-runner.d.ts +0 -24
  117. package/dist/runners/api-runner.js +0 -91
  118. package/dist/runners/api-runner.js.map +0 -1
  119. package/dist/runners/pipelex-runner.d.ts +0 -30
  120. package/dist/runners/pipelex-runner.js.map +0 -1
  121. /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