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.
Files changed (110) 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/validate.js +5 -13
  7. package/dist/agent/commands/validate.js.map +1 -1
  8. package/dist/agent-cli.js +5 -5
  9. package/dist/agent-cli.js.map +1 -1
  10. package/dist/cli/commands/config.js +2 -2
  11. package/dist/cli/commands/config.js.map +1 -1
  12. package/dist/cli/commands/install.js +19 -39
  13. package/dist/cli/commands/install.js.map +1 -1
  14. package/dist/cli/commands/run.js +82 -69
  15. package/dist/cli/commands/run.js.map +1 -1
  16. package/dist/cli/commands/setup.js +22 -23
  17. package/dist/cli/commands/setup.js.map +1 -1
  18. package/dist/cli/commands/utils.d.ts +1 -1
  19. package/dist/cli/commands/validate.js +10 -14
  20. package/dist/cli/commands/validate.js.map +1 -1
  21. package/dist/cli.js +2 -2
  22. package/dist/cli.js.map +1 -1
  23. package/dist/config/config.d.ts +14 -1
  24. package/dist/config/config.js +31 -6
  25. package/dist/config/config.js.map +1 -1
  26. package/dist/index.d.ts +27 -1
  27. package/dist/index.js +22 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/protocol/concept.d.ts +14 -0
  30. package/dist/protocol/concept.js +10 -0
  31. package/dist/protocol/concept.js.map +1 -0
  32. package/dist/protocol/exceptions.d.ts +10 -0
  33. package/dist/protocol/exceptions.js +12 -0
  34. package/dist/protocol/exceptions.js.map +1 -0
  35. package/dist/protocol/models.d.ts +95 -0
  36. package/dist/protocol/models.js +24 -0
  37. package/dist/protocol/models.js.map +1 -0
  38. package/dist/protocol/options.d.ts +60 -0
  39. package/dist/protocol/options.js +11 -0
  40. package/dist/protocol/options.js.map +1 -0
  41. package/dist/protocol/pipe_output.d.ts +11 -0
  42. package/dist/protocol/pipe_output.js +7 -0
  43. package/dist/protocol/pipe_output.js.map +1 -0
  44. package/dist/protocol/pipeline_inputs.d.ts +8 -0
  45. package/dist/protocol/pipeline_inputs.js +7 -0
  46. package/dist/protocol/pipeline_inputs.js.map +1 -0
  47. package/dist/protocol/protocol.d.ts +47 -0
  48. package/dist/{client → protocol}/protocol.js.map +1 -1
  49. package/dist/protocol/stuff.d.ts +16 -0
  50. package/dist/protocol/stuff.js +8 -0
  51. package/dist/{client/models → protocol}/stuff.js.map +1 -1
  52. package/dist/protocol/working_memory.d.ts +10 -0
  53. package/dist/protocol/working_memory.js +7 -0
  54. package/dist/protocol/working_memory.js.map +1 -0
  55. package/dist/runners/api/client.d.ts +170 -0
  56. package/dist/runners/api/client.js +653 -0
  57. package/dist/runners/api/client.js.map +1 -0
  58. package/dist/runners/api/exceptions.d.ts +106 -0
  59. package/dist/runners/api/exceptions.js +141 -0
  60. package/dist/runners/api/exceptions.js.map +1 -0
  61. package/dist/runners/api/models.d.ts +38 -0
  62. package/dist/runners/api/models.js +13 -0
  63. package/dist/runners/api/models.js.map +1 -0
  64. package/dist/runners/api/runs.d.ts +130 -0
  65. package/dist/runners/api/runs.js +93 -0
  66. package/dist/runners/api/runs.js.map +1 -0
  67. package/dist/runners/base-runner.d.ts +27 -0
  68. package/dist/runners/base-runner.js +25 -0
  69. package/dist/runners/base-runner.js.map +1 -0
  70. package/dist/runners/pipelex/runner.d.ts +38 -0
  71. package/dist/runners/{pipelex-runner.js → pipelex/runner.js} +168 -83
  72. package/dist/runners/pipelex/runner.js.map +1 -0
  73. package/dist/runners/registry.js +10 -4
  74. package/dist/runners/registry.js.map +1 -1
  75. package/dist/runners/types.d.ts +13 -71
  76. package/dist/runners/types.js.map +1 -1
  77. package/package.json +6 -3
  78. package/dist/client/client.d.ts +0 -15
  79. package/dist/client/client.js +0 -127
  80. package/dist/client/client.js.map +0 -1
  81. package/dist/client/exceptions.d.ts +0 -46
  82. package/dist/client/exceptions.js +0 -61
  83. package/dist/client/exceptions.js.map +0 -1
  84. package/dist/client/index.d.ts +0 -5
  85. package/dist/client/index.js +0 -3
  86. package/dist/client/index.js.map +0 -1
  87. package/dist/client/models/index.d.ts +0 -4
  88. package/dist/client/models/index.js +0 -2
  89. package/dist/client/models/index.js.map +0 -1
  90. package/dist/client/models/pipe_output.d.ts +0 -2
  91. package/dist/client/models/pipe_output.js +0 -2
  92. package/dist/client/models/pipe_output.js.map +0 -1
  93. package/dist/client/models/pipeline_inputs.d.ts +0 -3
  94. package/dist/client/models/pipeline_inputs.js +0 -2
  95. package/dist/client/models/pipeline_inputs.js.map +0 -1
  96. package/dist/client/models/stuff.d.ts +0 -1
  97. package/dist/client/models/stuff.js +0 -2
  98. package/dist/client/models/working_memory.d.ts +0 -1
  99. package/dist/client/models/working_memory.js +0 -2
  100. package/dist/client/models/working_memory.js.map +0 -1
  101. package/dist/client/pipeline.d.ts +0 -36
  102. package/dist/client/pipeline.js +0 -2
  103. package/dist/client/pipeline.js.map +0 -1
  104. package/dist/client/protocol.d.ts +0 -5
  105. package/dist/runners/api-runner.d.ts +0 -24
  106. package/dist/runners/api-runner.js +0 -91
  107. package/dist/runners/api-runner.js.map +0 -1
  108. package/dist/runners/pipelex-runner.d.ts +0 -30
  109. package/dist/runners/pipelex-runner.js.map +0 -1
  110. /package/dist/{client → protocol}/protocol.js +0 -0
package/README.md CHANGED
@@ -136,24 +136,31 @@ npx mthds setup runner pipelex
136
136
 
137
137
  ### Configure the API runner
138
138
 
139
- The API runner is the default. Set it up interactively:
139
+ The local pipelex runner is the default; to use the hosted (or self-hosted) API instead, set up the API runner interactively:
140
140
 
141
141
  ```bash
142
142
  mthds setup runner api
143
143
  ```
144
144
 
145
- This prompts for the API URL and API key (masked input) and saves them to `~/.mthds/config`.
145
+ This prompts for the API base URL and an API key (masked input), and saves them to `~/.mthds/config`.
146
+
147
+ There is ONE base URL — the host only, with no version prefix. The SDK composes every endpoint as `{base}/v1/{endpoint}`:
148
+
149
+ - **`base-url`** — hosted: `https://api.pipelex.com` (the default). Self-hosted: `http://localhost:8081` (a bare [pipelex-api](https://github.com/Pipelex/pipelex-api) runner).
146
150
 
147
151
  You can also set values directly:
148
152
 
149
153
  ```bash
150
154
  mthds config set api-key YOUR_KEY
151
- mthds config set api-url https://your-api-instance.com
155
+ # Hosted (default):
156
+ mthds config set base-url https://api.pipelex.com
157
+ # Self-hosted bare runner:
158
+ mthds config set base-url http://localhost:8081
152
159
  ```
153
160
 
154
161
  Configuration is stored in `~/.mthds/config` and shared between mthds-js and mthds-python.
155
162
 
156
- You can also use environment variables (`PIPELEX_API_KEY`, `PIPELEX_API_URL`) which take precedence over the config file.
163
+ You can also use environment variables (`MTHDS_API_KEY`, `MTHDS_API_URL`) which take precedence over the config file.
157
164
 
158
165
  See the [SDK Usage](#sdk-usage) section below to connect to a Pipelex API instance programmatically.
159
166
 
@@ -171,11 +178,11 @@ npm install mthds
171
178
  import { MthdsApiClient } from "mthds";
172
179
 
173
180
  const client = new MthdsApiClient({
174
- apiBaseUrl: "https://api.pipelex.com",
181
+ baseUrl: "https://api.pipelex.com",
175
182
  apiToken: "your-api-key",
176
183
  });
177
184
 
178
- const result = await client.executePipeline({
185
+ const result = await client.execute({
179
186
  pipe_code: "my-pipeline",
180
187
  inputs: {
181
188
  topic: "quantum computing",
@@ -185,50 +192,65 @@ const result = await client.executePipeline({
185
192
  console.log(result.pipe_output);
186
193
  ```
187
194
 
195
+ The base URL is the host only — every endpoint composes as `{baseUrl}/v1/{endpoint}` (e.g. `https://api.pipelex.com/v1/execute`); `/health` resolves to the origin root.
196
+
188
197
  ### Self-hosted API
189
198
 
190
- Point the client to your own [pipelex-api](https://github.com/Pipelex/pipelex-api) instance:
199
+ Point the client at your own [pipelex-api](https://github.com/Pipelex/pipelex-api) instance — the same `MTHDSProtocol` surface, same paths:
191
200
 
192
201
  ```typescript
193
202
  const client = new MthdsApiClient({
194
- apiBaseUrl: "http://localhost:8081",
203
+ baseUrl: "http://localhost:8081",
195
204
  apiToken: "your-api-key",
196
205
  });
197
206
  ```
198
207
 
208
+ The bare open-source runner has no run store, so the durable run-lifecycle methods (`getRunStatus`/`getRunResult`/`waitForResult`) throw a clear `RunLifecycleUnavailableError` against it — use `execute` (blocking) or `start` instead (completion delivery is implementation-defined — see your runner's API documentation). The `GET /v1/version` handshake tells the SDK which deployment it is talking to.
209
+
210
+ > Note: the bare-runner blocking path returns the runner's native `pipe_output`, whereas the hosted durable path returns `main_stuff` + `graph_spec`. Cross-shape normalization is a v1 TODO.
211
+
199
212
  ### Environment variables
200
213
 
201
214
  Instead of passing options to the constructor, you can set environment variables:
202
215
 
203
216
  | Variable | Description |
204
217
  |----------|-------------|
205
- | `PIPELEX_API_URL` | Base URL of the API |
206
- | `PIPELEX_API_KEY` | API authentication token |
218
+ | `MTHDS_API_URL` | API base URL host only, no version prefix (default `https://api.pipelex.com`) |
219
+ | `MTHDS_API_KEY` | API authentication token |
207
220
 
208
221
  ```typescript
209
- // Reads PIPELEX_API_URL and PIPELEX_API_KEY from the environment
222
+ // Reads MTHDS_API_URL and MTHDS_API_KEY from the environment
210
223
  const client = new MthdsApiClient();
211
224
  ```
212
225
 
213
226
  ### Methods
214
227
 
215
- | Method | Description |
216
- |--------|-------------|
217
- | `executePipeline(options)` | Execute a pipeline and wait for the result |
218
- | `startPipeline(options)` | Start a pipeline asynchronously |
228
+ The client implements the MTHDS Protocol plus the hosted run-lifecycle extension:
229
+
230
+ | Method | Route | Description |
231
+ |--------|-------|-------------|
232
+ | `execute(options)` | `POST /v1/execute` | Execute a method and wait for the result — returns a `RunResultExecute` (throws `RunStillRunningError` on the protocol's optional 202 degrade) |
233
+ | `start(options)` | `POST /v1/start` | Start a method asynchronously — returns a `RunResultStart` with the authoritative `pipeline_run_id` |
234
+ | `validate(contents, allowSignatures?)` | `POST /v1/validate` | Parse, validate, and dry-run a bundle |
235
+ | `models(category?)` | `GET /v1/models` | The model deck the runner routes to |
236
+ | `version()` | `GET /v1/version` | Protocol + implementation versions (the feature-detection handshake) |
237
+ | `getRunStatus(runId)` | `GET /v1/runs/{id}/status` | Hosted extension — self-healing status read |
238
+ | `getRunResult(runId)` | `GET /v1/runs/{id}/results` | Hosted extension — single-shot result lookup |
239
+ | `waitForResult(runId, options?)` | — | Hosted extension — poll to a terminal state |
219
240
 
220
- ### Pipeline options
241
+ ### Run options
221
242
 
222
243
  | Option | Type | Description |
223
244
  |--------|------|-------------|
224
- | `pipe_code` | `string` | Pipeline code to execute |
225
- | `mthds_content` | `string` | Raw method content (alternative to `pipe_code`) |
226
- | `inputs` | `Record<string, string \| string[] \| object>` | Pipeline input variables |
245
+ | `pipe_code` | `string` | Pipe code to execute |
246
+ | `mthds_contents` | `string[]` | Raw bundle contents (alternative to `pipe_code`) |
247
+ | `inputs` | `Record<string, string \| string[] \| object>` | Method input variables |
227
248
  | `output_name` | `string` | Name of the output to return |
228
249
  | `output_multiplicity` | `boolean \| number` | Expected output multiplicity |
229
- | `dynamic_output_concept_code` | `string` | Dynamic output concept code |
250
+ | `dynamic_output_concept_ref` | `string` | Dynamic output concept reference |
251
+ | `extra` | `Record<string, unknown>` | Server-specific extension args, forwarded verbatim into the request body (e.g. a stored-method id) |
230
252
 
231
- Either `pipe_code` or `mthds_content` must be provided.
253
+ Either `pipe_code` or `mthds_contents` must be provided (or a server-specific extension arg via `extra`). Anything beyond the protocol's basic args is server-specific and rides the generic `extra` option, merged into the request body — the server you call defines and handles its own extension args (a client-supplied run id, where a server supports one, is such an extension; the request side never names it). `startAndWaitForResult()` runs the whole start + poll lifecycle in one call.
232
254
 
233
255
  ## Telemetry
234
256
 
@@ -7,9 +7,8 @@
7
7
  import { existsSync, readFileSync, statSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { agentError, agentSuccess, AGENT_ERROR_DOMAINS } from "../output.js";
10
- function collect(val, prev) {
11
- return [...prev, val];
12
- }
10
+ import { MODEL_CATEGORIES } from "../../protocol/models.js";
11
+ import { ApiResponseError, RunFailedError, RunTimeoutError } from "../../runners/api/exceptions.js";
13
12
  /**
14
13
  * Register all API-runner commands on the program.
15
14
  * Only called when --runner=api.
@@ -114,7 +113,7 @@ export function registerApiRunnerCommands(program, makeRunner) {
114
113
  validateGroup
115
114
  .command("bundle")
116
115
  .argument("[target]", "Bundle file (.mthds) or directory")
117
- .option("--pipe <code>", "Pipe code to validate within the bundle")
116
+ .option("--allow-signatures", "Tolerate unimplemented pipe signatures")
118
117
  .option("--content <mthds>", "Bundle content as a string")
119
118
  .description("Validate a bundle file or content")
120
119
  .allowUnknownOption()
@@ -123,62 +122,24 @@ export function registerApiRunnerCommands(program, makeRunner) {
123
122
  .action(async (target, options) => {
124
123
  const runner = safeCreateRunner(makeRunner);
125
124
  const mthdsContent = resolveContent(target, options.content);
126
- try {
127
- const result = await runner.validate({
128
- mthds_contents: [mthdsContent],
129
- pipe_code: options.pipe,
130
- });
131
- if (result.success) {
132
- agentSuccess({ ...result });
133
- }
134
- else {
135
- agentError(result.message, "ValidationError", {
136
- error_domain: AGENT_ERROR_DOMAINS.VALIDATION,
137
- });
138
- }
139
- }
140
- catch (err) {
141
- agentError(err.message, "RunnerError", {
142
- error_domain: AGENT_ERROR_DOMAINS.RUNNER,
143
- });
144
- }
125
+ await runProtocolValidate(runner, [mthdsContent], options.allowSignatures ?? false);
145
126
  });
146
127
  validateGroup
147
128
  .command("pipe")
148
- .argument("<target>", "Pipe code or .mthds bundle file")
149
- .option("--pipe <code>", "Pipe code to validate")
150
- .description("Validate a pipe by code or bundle file")
129
+ .argument("<target>", ".mthds bundle file")
130
+ .option("--allow-signatures", "Tolerate unimplemented pipe signatures")
131
+ .description("Validate a bundle file (protocol validate covers every pipe in it)")
151
132
  .allowUnknownOption()
152
133
  .allowExcessArguments(true)
153
134
  .exitOverride()
154
135
  .action(async (target, options) => {
155
136
  const runner = safeCreateRunner(makeRunner);
156
- if (target.endsWith(".mthds")) {
157
- const mthdsContent = readFileOrError(target);
158
- try {
159
- const result = await runner.validate({
160
- mthds_contents: [mthdsContent],
161
- pipe_code: options.pipe,
162
- });
163
- handleValidateResult(result);
164
- }
165
- catch (err) {
166
- agentError(err.message, "RunnerError", {
167
- error_domain: AGENT_ERROR_DOMAINS.RUNNER,
168
- });
169
- }
170
- }
171
- else {
172
- try {
173
- const result = await runner.validate({ pipe_code: target });
174
- handleValidateResult(result);
175
- }
176
- catch (err) {
177
- agentError(err.message, "RunnerError", {
178
- error_domain: AGENT_ERROR_DOMAINS.RUNNER,
179
- });
180
- }
137
+ if (!target.endsWith(".mthds")) {
138
+ agentError("Validating a bare pipe code is not supported via the API runner — the protocol validate takes bundle contents. Pass a .mthds file.", "ArgumentError", { error_domain: AGENT_ERROR_DOMAINS.ARGUMENT });
139
+ return;
181
140
  }
141
+ const mthdsContent = readFileOrError(target);
142
+ await runProtocolValidate(runner, [mthdsContent], options.allowSignatures ?? false);
182
143
  });
183
144
  validateGroup
184
145
  .command("method")
@@ -188,20 +149,8 @@ export function registerApiRunnerCommands(program, makeRunner) {
188
149
  .allowUnknownOption()
189
150
  .allowExcessArguments(true)
190
151
  .exitOverride()
191
- .action(async (target, options) => {
192
- const runner = safeCreateRunner(makeRunner);
193
- try {
194
- const result = await runner.validate({
195
- method_url: target,
196
- pipe_code: options.pipe,
197
- });
198
- handleValidateResult(result);
199
- }
200
- catch (err) {
201
- agentError(err.message, "RunnerError", {
202
- error_domain: AGENT_ERROR_DOMAINS.RUNNER,
203
- });
204
- }
152
+ .action(async () => {
153
+ agentError("'validate method' is not supported via the API runner the protocol validate takes bundle contents, not a method URL. Pass a .mthds file to 'validate bundle', or use --runner pipelex.", "UnsupportedError", { error_domain: AGENT_ERROR_DOMAINS.RUNNER });
205
154
  });
206
155
  // ── inputs ──
207
156
  const inputsGroup = program
@@ -310,12 +259,17 @@ export function registerApiRunnerCommands(program, makeRunner) {
310
259
  inputs = parseJsonOrError(raw, "inputs file");
311
260
  }
312
261
  try {
313
- const result = await runner.execute({
262
+ const result = await runner.startAndWaitForResult({
314
263
  mthds_contents: [mthdsContent],
315
264
  pipe_code: pipeCode,
316
265
  inputs,
317
266
  });
318
- agentSuccess({ ...result });
267
+ agentSuccess({
268
+ state: "completed",
269
+ pipeline_run_id: result.pipeline_run_id,
270
+ main_stuff: result.main_stuff ?? result.pipe_output ?? null,
271
+ graph_spec: result.graph_spec ?? null,
272
+ });
319
273
  }
320
274
  catch (err) {
321
275
  agentError(err.message, "RunnerError", {
@@ -349,20 +303,31 @@ export function registerApiRunnerCommands(program, makeRunner) {
349
303
  .allowExcessArguments(true)
350
304
  .exitOverride()
351
305
  .action(runAction);
352
- // ── models ──
353
- program
354
- .command("models")
355
- .description("List available model presets, aliases, and waterfalls")
356
- .option("--type <type>", "Filter by model category (repeatable)", collect, [])
306
+ // ── run start ──
307
+ // Submit a run and return its id immediately. All run state lives behind the
308
+ // returned `pipeline_run_id` (DB + Temporal), so an agent can submit here,
309
+ // disconnect, and later resume with `run status` / `run result` / `run poll`.
310
+ runGroup
311
+ .command("start")
312
+ .argument("[target]", "Bundle file (.mthds) or directory")
313
+ .option("--pipe <code>", "Pipe code to run")
314
+ .option("-i, --inputs <file>", "Path to JSON inputs file")
315
+ .option("--content <mthds>", "Bundle content as a string")
316
+ .option("--inputs-json <json>", "Inputs as a JSON string")
317
+ .option("--extra <json>", "Server-specific extension args as a JSON object (e.g. a stored-method run) — forwarded to the runner verbatim")
318
+ .option("--output-name <name>", "Name of the output slot to write to")
319
+ .option("--output-multiplicity <value>", "Output multiplicity: 'false', 'true', or an exact count")
320
+ .option("--dynamic-output <concept_ref>", "Override for the dynamic output concept ref")
321
+ .description("Start a run and return its id without waiting")
357
322
  .allowUnknownOption()
358
323
  .allowExcessArguments(true)
359
324
  .exitOverride()
360
- .action(async (options) => {
325
+ .action(async (target, options) => {
361
326
  const runner = safeCreateRunner(makeRunner);
327
+ const startOptions = resolveStartOptions(target, options);
362
328
  try {
363
- const request = options.type?.length ? { type: options.type } : undefined;
364
- const result = await runner.models(request);
365
- agentSuccess({ ...result });
329
+ const ack = await runner.start(startOptions);
330
+ agentSuccess({ ...ack });
366
331
  }
367
332
  catch (err) {
368
333
  agentError(err.message, "RunnerError", {
@@ -370,24 +335,151 @@ export function registerApiRunnerCommands(program, makeRunner) {
370
335
  });
371
336
  }
372
337
  });
373
- // ── check-model ──
374
- program
375
- .command("check-model")
376
- .description("Validate a model reference with fuzzy suggestions")
377
- .argument("<reference>", "Model reference to check")
378
- .requiredOption("--type <type>", "Model category (llm, extract, img_gen, search)")
379
- .option("--format <format>", "DEPRECATED: agent CLI emits JSON only via agentSuccess envelope")
338
+ // ── run status ──
339
+ runGroup
340
+ .command("status")
341
+ .argument("<pipeline_run_id>", "Run id")
342
+ .description("Fetch a run's current status by id (self-healing)")
343
+ .allowUnknownOption()
344
+ .allowExcessArguments(true)
345
+ .exitOverride()
346
+ .action(async (runId) => {
347
+ const runner = safeCreateRunner(makeRunner);
348
+ try {
349
+ const run = await runner.getRunStatus(runId);
350
+ agentSuccess({ ...run });
351
+ }
352
+ catch (err) {
353
+ agentError(err.message, "RunnerError", {
354
+ error_domain: AGENT_ERROR_DOMAINS.RUNNER,
355
+ });
356
+ }
357
+ });
358
+ // ── run result ──
359
+ // Single-shot result lookup: 202 → still running, 200 → result, 409 → failed.
360
+ runGroup
361
+ .command("result")
362
+ .argument("<pipeline_run_id>", "Run id")
363
+ .description("Fetch a run's result by id, once (does not wait)")
380
364
  .allowUnknownOption()
381
365
  .allowExcessArguments(true)
382
366
  .exitOverride()
383
- .action(async (reference, options) => {
384
- if (options.format) {
385
- agentError("`--format` is no longer supported on `mthds-agent check-model`. The agent CLI always emits JSON via the agentSuccess envelope.", "ArgumentError", { error_domain: AGENT_ERROR_DOMAINS.ARGUMENT });
367
+ .action(async (runId) => {
368
+ const runner = safeCreateRunner(makeRunner);
369
+ let state;
370
+ try {
371
+ state = await runner.getRunResult(runId);
372
+ }
373
+ catch (err) {
374
+ agentError(err.message, "RunnerError", {
375
+ error_domain: AGENT_ERROR_DOMAINS.RUNNER,
376
+ });
386
377
  return;
387
378
  }
379
+ switch (state.state) {
380
+ case "running":
381
+ agentSuccess({
382
+ state: "running",
383
+ pipeline_run_id: state.pipeline_run_id,
384
+ retry_after_seconds: state.retry_after_seconds,
385
+ hint: `Run is still in progress. Poll with: mthds-agent run poll ${runId}`,
386
+ });
387
+ break;
388
+ case "completed":
389
+ agentSuccess({
390
+ state: "completed",
391
+ pipeline_run_id: state.pipeline_run_id,
392
+ main_stuff: state.result.main_stuff ?? null,
393
+ graph_spec: state.result.graph_spec ?? null,
394
+ });
395
+ break;
396
+ case "failed":
397
+ agentError(state.message, "RunFailedError", {
398
+ error_domain: AGENT_ERROR_DOMAINS.PIPELINE,
399
+ retryable: false,
400
+ });
401
+ break;
402
+ }
403
+ });
404
+ // ── run poll ──
405
+ // Block until the run reaches a terminal state. Ctrl-C (or any SIGINT) stops
406
+ // waiting WITHOUT cancelling the run — the run keeps executing server-side
407
+ // and can be resumed by id.
408
+ runGroup
409
+ .command("poll")
410
+ .argument("<pipeline_run_id>", "Run id")
411
+ .option("--interval <seconds>", "Base poll interval in seconds (default 2)")
412
+ .option("--timeout <seconds>", "Max seconds to wait before giving up (default 1200)")
413
+ .description("Poll a run to completion, then return its result")
414
+ .allowUnknownOption()
415
+ .allowExcessArguments(true)
416
+ .exitOverride()
417
+ .action(async (runId, options) => {
418
+ const runner = safeCreateRunner(makeRunner);
419
+ const intervalMs = parsePositiveSeconds(options.interval, "--interval");
420
+ const timeoutMs = parsePositiveSeconds(options.timeout, "--timeout");
421
+ const controller = new AbortController();
422
+ const onSigint = () => controller.abort();
423
+ process.once("SIGINT", onSigint);
424
+ try {
425
+ const result = await runner.waitForResult(runId, {
426
+ intervalMs,
427
+ timeoutMs,
428
+ signal: controller.signal,
429
+ });
430
+ agentSuccess({
431
+ state: "completed",
432
+ pipeline_run_id: runId,
433
+ main_stuff: result.main_stuff ?? null,
434
+ graph_spec: result.graph_spec ?? null,
435
+ });
436
+ }
437
+ catch (err) {
438
+ if (controller.signal.aborted) {
439
+ // Walk-away: not an error. Report the run as still resumable by id.
440
+ agentSuccess({
441
+ state: "running",
442
+ pipeline_run_id: runId,
443
+ resumable: true,
444
+ hint: `Stopped waiting; the run continues. Resume with: mthds-agent run poll ${runId}`,
445
+ });
446
+ }
447
+ else if (err instanceof RunFailedError) {
448
+ agentError(err.message, "RunFailedError", {
449
+ error_domain: AGENT_ERROR_DOMAINS.PIPELINE,
450
+ retryable: false,
451
+ });
452
+ }
453
+ else if (err instanceof RunTimeoutError) {
454
+ agentError(err.message, "RunTimeoutError", {
455
+ error_domain: AGENT_ERROR_DOMAINS.RUNNER,
456
+ retryable: true,
457
+ hint: `The run is still executing. Resume with: mthds-agent run poll ${runId}`,
458
+ });
459
+ }
460
+ else {
461
+ agentError(err.message, "RunnerError", {
462
+ error_domain: AGENT_ERROR_DOMAINS.RUNNER,
463
+ });
464
+ }
465
+ }
466
+ finally {
467
+ process.removeListener("SIGINT", onSigint);
468
+ }
469
+ });
470
+ // ── models ──
471
+ program
472
+ .command("models")
473
+ .description("List the model deck (models, aliases, waterfalls)")
474
+ .option("--type <type>", "Filter by model category (llm, extract, img_gen, search)")
475
+ .allowUnknownOption()
476
+ .allowExcessArguments(true)
477
+ .exitOverride()
478
+ .action(async (options) => {
388
479
  const runner = safeCreateRunner(makeRunner);
480
+ const category = parseModelCategory(options.type);
389
481
  try {
390
- const result = await runner.checkModel({ reference, type: options.type });
482
+ const result = await runner.models(category);
391
483
  agentSuccess({ ...result });
392
484
  }
393
485
  catch (err) {
@@ -396,6 +488,21 @@ export function registerApiRunnerCommands(program, makeRunner) {
396
488
  });
397
489
  }
398
490
  });
491
+ // ── check-model ──
492
+ // check-model is a LOCAL CLI capability (pipelex runner) only — the MTHDS
493
+ // API has no check-model route. Registered here so the API runner errors
494
+ // cleanly instead of failing with an opaque 404.
495
+ program
496
+ .command("check-model")
497
+ .description("Validate a model reference with fuzzy suggestions (pipelex runner only)")
498
+ .argument("<reference>", "Model reference to check")
499
+ .option("--type <type>", "Model category (llm, extract, img_gen, search)")
500
+ .allowUnknownOption()
501
+ .allowExcessArguments(true)
502
+ .exitOverride()
503
+ .action(async () => {
504
+ agentError("check-model is not available on the API runner — the MTHDS API has no check-model route. It is a local capability of the pipelex runner: re-run with --runner pipelex. To list what the API can route to, use: mthds-agent models", "UnsupportedError", { error_domain: AGENT_ERROR_DOMAINS.RUNNER });
505
+ });
399
506
  }
400
507
  // ── Helpers ──
401
508
  function safeCreateRunner(makeRunner) {
@@ -485,13 +592,103 @@ function resolvePipeCode(mthdsContent, pipeCodeOption) {
485
592
  });
486
593
  throw new Error("unreachable");
487
594
  }
488
- function handleValidateResult(result) {
489
- if (result.success) {
490
- agentSuccess({ ...result });
595
+ function resolveRunInputs(options) {
596
+ if (options.inputsJson)
597
+ return parseJsonOrError(options.inputsJson, "--inputs-json");
598
+ if (options.inputs) {
599
+ const raw = readFileOrError(options.inputs);
600
+ return parseJsonOrError(raw, "inputs file");
601
+ }
602
+ return undefined;
603
+ }
604
+ function resolveStartOptions(target, options) {
605
+ const outputs = {
606
+ output_name: options.outputName,
607
+ output_multiplicity: parseMultiplicity(options.outputMultiplicity),
608
+ dynamic_output_concept_ref: options.dynamicOutput,
609
+ };
610
+ const extra = parseExtraOption(options.extra);
611
+ // No inline bundle → an extension-only start: the run is identified entirely by
612
+ // server-specific args passed through `--extra` (e.g. a stored-method run). The
613
+ // runner is the source of truth for what `extra` it accepts — the SDK and CLI
614
+ // never name those args.
615
+ if (!target && !options.content) {
616
+ return { pipe_code: options.pipe, inputs: resolveRunInputs(options), ...outputs, extra };
617
+ }
618
+ // resolveContentForRun may set options.inputs (directory auto-discovery), so
619
+ // resolve the bundle before reading inputs.
620
+ const mthdsContent = resolveContentForRun(target, options);
621
+ const pipeCode = resolvePipeCode(mthdsContent, options.pipe);
622
+ return { pipe_code: pipeCode, mthds_contents: [mthdsContent], inputs: resolveRunInputs(options), ...outputs, extra };
623
+ }
624
+ /** Parse `--extra <json>` into the generic extension passthrough — a JSON object of server-defined args. */
625
+ function parseExtraOption(raw) {
626
+ if (!raw)
627
+ return undefined;
628
+ const parsed = parseJsonOrError(raw, "--extra");
629
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
630
+ agentError("--extra must be a JSON object of server-specific args.", "ArgumentError", {
631
+ error_domain: AGENT_ERROR_DOMAINS.ARGUMENT,
632
+ });
633
+ throw new Error("unreachable");
634
+ }
635
+ return parsed;
636
+ }
637
+ /** Parse `--output-multiplicity`: "false"/"true" → boolean, a positive integer → count. */
638
+ function parseMultiplicity(raw) {
639
+ if (raw === undefined)
640
+ return undefined;
641
+ if (raw === "true")
642
+ return true;
643
+ if (raw === "false")
644
+ return false;
645
+ const count = Number(raw);
646
+ if (Number.isInteger(count) && count > 0)
647
+ return count;
648
+ agentError("--output-multiplicity must be 'true', 'false', or a positive integer.", "ArgumentError", { error_domain: AGENT_ERROR_DOMAINS.ARGUMENT });
649
+ throw new Error("unreachable");
650
+ }
651
+ function parsePositiveSeconds(raw, label) {
652
+ if (raw === undefined)
653
+ return undefined;
654
+ const seconds = Number(raw);
655
+ if (!Number.isFinite(seconds) || seconds <= 0) {
656
+ agentError(`${label} must be a positive number of seconds.`, "ArgumentError", {
657
+ error_domain: AGENT_ERROR_DOMAINS.ARGUMENT,
658
+ });
659
+ throw new Error("unreachable");
660
+ }
661
+ return seconds * 1000;
662
+ }
663
+ /** Parse the `--type` model-category filter, erroring on unknown values. */
664
+ function parseModelCategory(raw) {
665
+ if (raw === undefined)
666
+ return undefined;
667
+ if (MODEL_CATEGORIES.includes(raw)) {
668
+ return raw;
669
+ }
670
+ agentError(`--type must be one of: ${MODEL_CATEGORIES.join(", ")}.`, "ArgumentError", { error_domain: AGENT_ERROR_DOMAINS.ARGUMENT });
671
+ throw new Error("unreachable");
672
+ }
673
+ /**
674
+ * Run the protocol validate (`POST /v1/validate`) and emit the agent envelope.
675
+ * A valid bundle returns the structural artifacts; an invalid bundle is an
676
+ * HTTP 422 problem, surfaced as a ValidationError.
677
+ */
678
+ async function runProtocolValidate(runner, mthdsContents, allowSignatures) {
679
+ try {
680
+ const report = await runner.validate(mthdsContents, allowSignatures);
681
+ agentSuccess({ success: true, ...report });
491
682
  }
492
- else {
493
- agentError(result.message, "ValidationError", {
494
- error_domain: AGENT_ERROR_DOMAINS.VALIDATION,
683
+ catch (err) {
684
+ if (err instanceof ApiResponseError && err.status === 422) {
685
+ agentError(err.serverMessage ?? err.message, "ValidationError", {
686
+ error_domain: AGENT_ERROR_DOMAINS.VALIDATION,
687
+ });
688
+ return;
689
+ }
690
+ agentError(err.message, "RunnerError", {
691
+ error_domain: AGENT_ERROR_DOMAINS.RUNNER,
495
692
  });
496
693
  }
497
694
  }