@hilbras/omninode 2.0.0-alpha.4 → 2.0.0-alpha.6

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/dist/index.js CHANGED
@@ -394,56 +394,184 @@ function authHeaders(config, env = process.env) {
394
394
  }
395
395
 
396
396
  // src/providers/http.ts
397
+ var HttpResponseError = class extends Error {
398
+ constructor(message, status, cause) {
399
+ super(message, { cause });
400
+ this.status = status;
401
+ this.name = "HttpResponseError";
402
+ }
403
+ status;
404
+ };
397
405
  var DEFAULT_TIMEOUT_MS = 3e4;
398
406
  async function requestJson(url, options = {}) {
399
407
  const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
400
- let response;
401
- try {
402
- response = await fetch(url, {
403
- method,
404
- headers: {
405
- accept: "application/json",
406
- ...body !== void 0 ? { "content-type": "application/json" } : {},
407
- ...headers
408
- },
409
- ...body !== void 0 ? { body: JSON.stringify(body) } : {},
410
- signal: AbortSignal.timeout(timeoutMs)
411
- });
412
- } catch (error) {
413
- const reason = error instanceof Error && error.name === "TimeoutError" ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
414
- throw new ProviderError("PROVIDER_UNAVAILABLE", `Request to ${url} failed: ${reason}.`, {
415
- cause: error
416
- });
417
- }
408
+ const response = await fetch(url, {
409
+ method,
410
+ headers: {
411
+ accept: "application/json",
412
+ ...body !== void 0 ? { "content-type": "application/json" } : {},
413
+ ...headers
414
+ },
415
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
416
+ signal: AbortSignal.timeout(timeoutMs)
417
+ });
418
418
  let parsed;
419
419
  try {
420
420
  parsed = await response.json();
421
421
  } catch (error) {
422
- throw new ProviderError("PROVIDER_UNAVAILABLE", `Response from ${url} was not valid JSON.`, {
423
- cause: error,
424
- details: { status: response.status }
425
- });
422
+ throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
426
423
  }
427
424
  return { status: response.status, body: parsed };
428
425
  }
429
426
 
427
+ // src/providers/errors.ts
428
+ function kindToCode(kind) {
429
+ switch (kind) {
430
+ case "AUTHENTICATION_ERROR":
431
+ return "PROVIDER_AUTH_FAILED";
432
+ case "MODEL_NOT_FOUND":
433
+ return "MODEL_NOT_FOUND";
434
+ default:
435
+ return "PROVIDER_UNAVAILABLE";
436
+ }
437
+ }
438
+ function isRetryable(kind) {
439
+ return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
440
+ }
441
+ function kindFromStatus(status) {
442
+ if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
443
+ if (status === 429) return "RATE_LIMIT_ERROR";
444
+ if (status === 404) return "MODEL_NOT_FOUND";
445
+ if (status === 408 || status === 504) return "TIMEOUT";
446
+ if (status >= 500) return "SERVER_ERROR";
447
+ if (status >= 400) return "INVALID_REQUEST";
448
+ return "UNKNOWN_ERROR";
449
+ }
450
+ function extractMessage(body) {
451
+ if (body === null || typeof body !== "object") return void 0;
452
+ const root = body;
453
+ const error = root.error ?? root;
454
+ const message = error.message ?? error.detail ?? root.message;
455
+ return typeof message === "string" ? message : void 0;
456
+ }
457
+ function parseRetryAfter(value, now = Date.now()) {
458
+ if (!value) return void 0;
459
+ const seconds = Number(value);
460
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
461
+ const date = Date.parse(value);
462
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
463
+ }
464
+ function isTimeoutError(error) {
465
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
466
+ }
467
+ function isNetworkError(error) {
468
+ return error instanceof TypeError || typeof error?.cause === "object" && error instanceof Error && !error.code;
469
+ }
470
+ function fallbackMessage(kind, context, status) {
471
+ const { provider, operation } = context;
472
+ switch (kind) {
473
+ case "AUTHENTICATION_ERROR":
474
+ return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
475
+ case "RATE_LIMIT_ERROR":
476
+ return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
477
+ case "MODEL_NOT_FOUND":
478
+ return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
479
+ case "TIMEOUT":
480
+ return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
481
+ case "SERVER_ERROR":
482
+ return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
483
+ case "INVALID_REQUEST":
484
+ return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
485
+ default:
486
+ return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
487
+ }
488
+ }
489
+ function providerHttpError(context) {
490
+ const status = context.status ?? 0;
491
+ const kind = kindFromStatus(status);
492
+ const detail = extractMessage(context.body);
493
+ const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
494
+ return new ProviderError(
495
+ context.code ?? kindToCode(kind),
496
+ detail ?? fallbackMessage(kind, context, status),
497
+ {
498
+ cause: context.cause,
499
+ details: {
500
+ kind,
501
+ provider: context.provider,
502
+ operation: context.operation,
503
+ status,
504
+ retryable: isRetryable(kind),
505
+ ...retryAfterMs !== void 0 ? { retryAfterMs } : {},
506
+ ...detail ? { providerMessage: detail } : {}
507
+ }
508
+ }
509
+ );
510
+ }
511
+ function providerTransportError(context) {
512
+ const cause = context.cause;
513
+ const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
514
+ const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
515
+ return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
516
+ cause: context.cause,
517
+ details: {
518
+ kind,
519
+ provider: context.provider,
520
+ operation: context.operation,
521
+ retryable: isRetryable(kind)
522
+ }
523
+ });
524
+ }
525
+
430
526
  // src/providers/openai-compatible/index.ts
431
527
  var OpenAICompatibleProvider = class {
432
528
  config;
529
+ providerId;
530
+ authentication;
433
531
  log;
434
- constructor(config, log = logger) {
532
+ /** Discovered models, so getModel() answers without another round-trip. */
533
+ discovered;
534
+ env;
535
+ constructor(config, log = logger, env = process.env) {
435
536
  this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
537
+ this.env = env;
538
+ this.providerId = config.providerId ?? config.name;
539
+ this.authentication = {
540
+ method: config.apiKeyEnvVar ? "bearer" : "none",
541
+ ...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
542
+ configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
543
+ };
436
544
  this.log = log.child({ provider: config.name });
437
545
  }
546
+ /** Authenticate + discover (roadmap §10, Model Discovery). */
547
+ async connect() {
548
+ authHeaders(this.config, this.env);
549
+ const models = await this.listModels();
550
+ this.discovered = models;
551
+ return models;
552
+ }
553
+ async getModel(modelId) {
554
+ const models = this.discovered ?? await this.connect();
555
+ return models.find((m) => m.id === modelId);
556
+ }
438
557
  async listModels() {
439
- const { status, body } = await requestJson(`${this.config.baseUrl}/models`, {
440
- headers: authHeaders(this.config)
441
- });
442
- if (status === 401 || status === 403) {
443
- throw authFailed(this.config, status);
558
+ let response;
559
+ try {
560
+ response = await requestJson(`${this.config.baseUrl}/models`, {
561
+ headers: authHeaders(this.config, this.env)
562
+ });
563
+ } catch (error) {
564
+ if (error instanceof ProviderError) throw error;
565
+ throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
444
566
  }
567
+ const { status, body } = response;
445
568
  if (status !== 200) {
446
- throw unavailable(this.config, `Model discovery returned HTTP ${status}.`, status);
569
+ throw providerHttpError({
570
+ provider: this.config.name,
571
+ operation: "listModels",
572
+ status,
573
+ body
574
+ });
447
575
  }
448
576
  const entries = extractModelEntries(body, this.config);
449
577
  const models = [];
@@ -453,22 +581,40 @@ var OpenAICompatibleProvider = class {
453
581
  skipped += 1;
454
582
  continue;
455
583
  }
456
- models.push({
457
- provider: this.config.name,
458
- id: entry.id,
459
- displayName: entry.id,
460
- status: "available",
461
- metadata: {
462
- ...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
463
- ...typeof entry.created === "number" ? { createdAt: entry.created } : {}
464
- }
465
- });
584
+ models.push(this.toModelInfo(entry));
466
585
  }
467
586
  if (skipped > 0) {
468
587
  this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
469
588
  }
470
589
  return models;
471
590
  }
591
+ /** Maps a raw model entry onto the normalized metadata shape (§10). */
592
+ toModelInfo(entry) {
593
+ const parameters = Array.isArray(entry.supported_parameters) ? entry.supported_parameters.filter((p) => typeof p === "string") : void 0;
594
+ const contextWindow = typeof entry.context_length === "number" ? entry.context_length : typeof entry.context_window === "number" ? entry.context_window : void 0;
595
+ const inputTypes = normalizeModalities(entry.input_modalities);
596
+ const outputTypes = normalizeModalities(entry.output_modalities);
597
+ return {
598
+ provider: this.config.name,
599
+ id: entry.id,
600
+ displayName: entry.id,
601
+ ...typeof entry.name === "string" ? { name: entry.name } : {},
602
+ status: "available",
603
+ ...contextWindow !== void 0 ? { contextWindow } : {},
604
+ ...inputTypes !== void 0 ? { inputTypes } : {},
605
+ ...outputTypes !== void 0 ? { outputTypes } : {},
606
+ ...parameters !== void 0 ? {
607
+ supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
608
+ supportsStructuredOutput: parameters.includes("response_format"),
609
+ supportsStreaming: parameters.includes("stream")
610
+ } : {},
611
+ metadata: {
612
+ ...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
613
+ ...typeof entry.created === "number" ? { createdAt: entry.created } : {},
614
+ ...parameters !== void 0 ? { supportedParameters: parameters } : {}
615
+ }
616
+ };
617
+ }
472
618
  async healthCheck() {
473
619
  const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
474
620
  try {
@@ -498,27 +644,34 @@ var OpenAICompatibleProvider = class {
498
644
  }
499
645
  }
500
646
  async chat(request) {
501
- const { status, body } = await requestJson(`${this.config.baseUrl}/chat/completions`, {
502
- method: "POST",
503
- headers: authHeaders(this.config),
504
- body: {
505
- model: request.model,
506
- messages: request.messages,
507
- ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
508
- ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
509
- ...request.stop !== void 0 ? { stop: request.stop } : {}
510
- }
511
- });
512
- if (status === 401 || status === 403) throw authFailed(this.config, status);
513
- if (status === 404) {
514
- throw new ProviderError(
515
- "MODEL_NOT_FOUND",
516
- `Model "${request.model}" or the chat endpoint was not found on provider "${this.config.name}" (HTTP 404).`,
517
- { details: { status } }
518
- );
647
+ let response;
648
+ try {
649
+ response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
650
+ method: "POST",
651
+ headers: authHeaders(this.config, this.env),
652
+ body: {
653
+ model: request.model,
654
+ messages: request.messages,
655
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
656
+ ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
657
+ ...request.stop !== void 0 ? { stop: request.stop } : {}
658
+ }
659
+ });
660
+ } catch (error) {
661
+ if (error instanceof ProviderError) throw error;
662
+ throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
519
663
  }
664
+ const { status, body } = response;
520
665
  if (status !== 200) {
521
- throw unavailable(this.config, `Chat completion returned HTTP ${status}.`, status);
666
+ throw providerHttpError({
667
+ provider: this.config.name,
668
+ operation: "chat",
669
+ status,
670
+ body,
671
+ ...status === 404 ? {
672
+ code: "MODEL_NOT_FOUND"
673
+ } : {}
674
+ });
522
675
  }
523
676
  const payload = body;
524
677
  const choices = Array.isArray(payload.choices) ? payload.choices : [];
@@ -540,6 +693,13 @@ var OpenAICompatibleProvider = class {
540
693
  };
541
694
  }
542
695
  };
696
+ function normalizeModalities(value) {
697
+ const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
698
+ if (!list) return void 0;
699
+ const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
700
+ const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
701
+ return modalities.length > 0 ? modalities : void 0;
702
+ }
543
703
  function normalizeBaseUrl(baseUrl) {
544
704
  return baseUrl.replace(/\/+$/, "");
545
705
  }
@@ -554,14 +714,6 @@ function extractModelEntries(body, config) {
554
714
  { details: { endpoint: "models" } }
555
715
  );
556
716
  }
557
- function authFailed(config, status) {
558
- const envHint = config.apiKeyEnvVar ? ` Check that $${config.apiKeyEnvVar} is set and valid.` : " The provider requires authentication; configure api_key_env_var for it.";
559
- return new ProviderError(
560
- "PROVIDER_AUTH_FAILED",
561
- `Authentication with provider "${config.name}" failed (HTTP ${status}).${envHint}`,
562
- { details: { status } }
563
- );
564
- }
565
717
  function unavailable(config, message, status) {
566
718
  return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
567
719
  details: { status }
@@ -578,7 +730,7 @@ var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
578
730
  * Run the full connect flow. Throws on authentication or transport failure
579
731
  * (use healthCheck() for a non-throwing probe).
580
732
  */
581
- async connect(registry) {
733
+ async connectAndRegister(registry) {
582
734
  const apiKey = resolveApiKey(this.config);
583
735
  const discovered = await this.listModels();
584
736
  const seen = /* @__PURE__ */ new Set();
@@ -1430,7 +1582,7 @@ function describeExit(code, signal) {
1430
1582
  return `Process exited with code ${code}.`;
1431
1583
  }
1432
1584
  function sanitizeChunk(chunk) {
1433
- return chunk.toString("utf8").replace(/\u0000/g, "");
1585
+ return [...chunk.toString("utf8")].filter((char) => char.charCodeAt(0) !== 0).join("");
1434
1586
  }
1435
1587
  function normalizeReport(payload, taskId, agentName, index) {
1436
1588
  if (payload === null || typeof payload !== "object") return void 0;
@@ -1777,6 +1929,11 @@ var TaskEngine = class {
1777
1929
  if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
1778
1930
  return current;
1779
1931
  }
1932
+ const currentState = await this.options.store.get(current.id);
1933
+ if (currentState?.status === "cancelled") {
1934
+ current = currentState;
1935
+ break;
1936
+ }
1780
1937
  const result = {
1781
1938
  ...output.summary !== void 0 ? { summary: output.summary } : {},
1782
1939
  ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
@@ -2439,9 +2596,23 @@ var PipelineEngine = class {
2439
2596
  }
2440
2597
  options;
2441
2598
  log;
2599
+ /** Runs executing in THIS process — cancellation can terminate them directly. */
2600
+ activeRuns = /* @__PURE__ */ new Map();
2442
2601
  get store() {
2443
2602
  return this.options.store;
2444
2603
  }
2604
+ /**
2605
+ * Saves the run while preserving fields that may be set by another process
2606
+ * (e.g. a cancellation request) — an in-memory snapshot must never clobber
2607
+ * externally requested cancellation (§9.2).
2608
+ */
2609
+ async persist(run) {
2610
+ const current = await this.options.store.get(run.id);
2611
+ await this.options.store.save({
2612
+ ...run,
2613
+ ...current?.cancellationRequested === true ? { cancellationRequested: true, cancellationRequestedAt: current.cancellationRequestedAt } : {}
2614
+ });
2615
+ }
2445
2616
  /** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
2446
2617
  validate(def) {
2447
2618
  const ids = /* @__PURE__ */ new Set();
@@ -2541,18 +2712,44 @@ var PipelineEngine = class {
2541
2712
  );
2542
2713
  }
2543
2714
  }
2715
+ if (step.model) {
2716
+ const separator = step.model.indexOf(":");
2717
+ if (separator <= 0 || separator === step.model.length - 1) {
2718
+ throw new PipelineError(
2719
+ "PIPELINE_INVALID",
2720
+ `Step "${step.id}" has an invalid model reference "${step.model}" \u2014 use "provider:model-id".`
2721
+ );
2722
+ }
2723
+ const providerName = step.model.slice(0, separator);
2724
+ if (!this.options.providers.some((p) => p.name === providerName)) {
2725
+ throw new PipelineError(
2726
+ "PIPELINE_INVALID",
2727
+ `Step "${step.id}" uses model "${step.model}" but provider "${providerName}" is not configured.`
2728
+ );
2729
+ }
2730
+ }
2544
2731
  }
2545
2732
  const now = (/* @__PURE__ */ new Date()).toISOString();
2546
2733
  const run = {
2547
2734
  id: `run-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
2548
2735
  pipelineId: def.id,
2736
+ attempt: 1,
2549
2737
  objective,
2550
2738
  status: "running",
2551
2739
  startedAt: now,
2552
2740
  stepRuns: []
2553
2741
  };
2554
2742
  await this.options.store.save(run);
2555
- this.log.info(`Running pipeline ${def.id} as ${run.id}.`);
2743
+ const liveTaskIds = /* @__PURE__ */ new Set();
2744
+ this.activeRuns.set(run.id, {
2745
+ taskIds: liveTaskIds,
2746
+ cancelTasks: async () => {
2747
+ for (const taskId of liveTaskIds) {
2748
+ await this.options.tasks.cancel(taskId).catch(() => void 0);
2749
+ }
2750
+ }
2751
+ });
2752
+ this.log.info(`Running pipeline ${def.id} as ${run.id} (attempt ${run.attempt}).`);
2556
2753
  await this.options.audit?.record({
2557
2754
  at: (/* @__PURE__ */ new Date()).toISOString(),
2558
2755
  action: "pipeline.run.started",
@@ -2564,7 +2761,22 @@ var PipelineEngine = class {
2564
2761
  const pending = new Set(def.steps.map((s) => s.id));
2565
2762
  const contextParts = [];
2566
2763
  const allReports = [];
2764
+ let cancelled = false;
2567
2765
  while (pending.size > 0) {
2766
+ const persisted = await this.options.store.get(run.id);
2767
+ if (persisted?.cancellationRequested === true) {
2768
+ cancelled = true;
2769
+ run.cancellationRequested = true;
2770
+ run.cancellationRequestedAt = persisted.cancellationRequestedAt;
2771
+ await this.activeRuns.get(run.id)?.cancelTasks();
2772
+ for (const step of def.steps.filter((s) => pending.has(s.id))) {
2773
+ const stepRun = { stepId: step.id, status: "cancelled" };
2774
+ run.stepRuns.push(stepRun);
2775
+ options.onStep?.(stepRun);
2776
+ }
2777
+ pending.clear();
2778
+ break;
2779
+ }
2568
2780
  const ready = def.steps.filter(
2569
2781
  (step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
2570
2782
  );
@@ -2581,7 +2793,8 @@ var PipelineEngine = class {
2581
2793
  ...role ? { role } : {},
2582
2794
  combinedContext: contextParts.join("\n\n"),
2583
2795
  reports: [...allReports],
2584
- pipelineRunId: run.id
2796
+ pipelineRunId: run.id,
2797
+ runId: run.id
2585
2798
  };
2586
2799
  return this.executeStep(step, ctx);
2587
2800
  })
@@ -2598,6 +2811,8 @@ ${contribution}`);
2598
2811
  }
2599
2812
  const stepRun = {
2600
2813
  stepId: step.id,
2814
+ attempt: outcome.attempt ?? 1,
2815
+ ...outcome.executionIds && outcome.executionIds.length > 0 ? { executionIds: outcome.executionIds } : {},
2601
2816
  status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
2602
2817
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
2603
2818
  ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
@@ -2607,12 +2822,12 @@ ${contribution}`);
2607
2822
  options.onStep?.(stepRun);
2608
2823
  });
2609
2824
  run.reports = [...allReports];
2610
- await this.options.store.save(run);
2825
+ await this.persist(run);
2611
2826
  }
2612
2827
  const allOutcomes = [...outcomes.values()];
2613
- const failed = allOutcomes.some((o) => o.status === "failed");
2614
- const partial = !failed && allOutcomes.some((o) => o.status === "partial");
2615
- const finalStatus = failed ? "failed" : partial ? "partial" : "completed";
2828
+ const failed = !cancelled && allOutcomes.some((o) => o.status === "failed");
2829
+ const partial = !cancelled && !failed && allOutcomes.some((o) => o.status === "partial");
2830
+ const finalStatus = cancelled ? "cancelled" : failed ? "failed" : partial ? "partial" : "completed";
2616
2831
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
2617
2832
  let resultSummary;
2618
2833
  for (let i = def.steps.length - 1; i >= 0; i -= 1) {
@@ -2640,16 +2855,19 @@ ${contribution}`);
2640
2855
  run.reports = collected;
2641
2856
  }
2642
2857
  }
2858
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2859
+ this.activeRuns.delete(run.id);
2643
2860
  const finished = {
2644
2861
  ...run,
2645
2862
  status: finalStatus,
2646
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
2863
+ finishedAt,
2864
+ attempts: [...run.attempts ?? [], { attempt: run.attempt, startedAt: run.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
2647
2865
  ...run.reports !== void 0 && run.reports.length > 0 ? { reports: run.reports } : {},
2648
2866
  ...combinedReportId !== void 0 ? { combinedReportId } : {},
2649
2867
  ...planId !== void 0 ? { planId } : {},
2650
2868
  ...resultSummary !== void 0 ? { resultSummary } : {}
2651
2869
  };
2652
- await this.options.store.save(finished);
2870
+ await this.persist(finished);
2653
2871
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
2654
2872
  await this.options.audit?.record({
2655
2873
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2659,6 +2877,23 @@ ${contribution}`);
2659
2877
  });
2660
2878
  return finished;
2661
2879
  }
2880
+ /** First-class cancellation (§9.2): requests cancellation and terminates
2881
+ * tasks that are running in this process immediately. */
2882
+ async cancel(runId) {
2883
+ const run = await this.options.store.get(runId);
2884
+ if (!run) {
2885
+ throw new PipelineError("PIPELINE_NOT_FOUND", `Pipeline run "${runId}" does not exist.`);
2886
+ }
2887
+ await this.options.store.save({
2888
+ ...run,
2889
+ cancellationRequested: true,
2890
+ cancellationRequestedAt: (/* @__PURE__ */ new Date()).toISOString()
2891
+ });
2892
+ const active = this.activeRuns.get(runId);
2893
+ if (active) {
2894
+ await active.cancelTasks();
2895
+ }
2896
+ }
2662
2897
  async get(runId) {
2663
2898
  return this.options.store.get(runId);
2664
2899
  }
@@ -2682,9 +2917,16 @@ ${contribution}`);
2682
2917
  this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
2683
2918
  }
2684
2919
  last = await this.executeStepOnce(step, ctx);
2685
- if (last.status !== "failed") return last;
2920
+ if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
2686
2921
  }
2687
- return last;
2922
+ return { ...last, attempt: retries + 1 };
2923
+ }
2924
+ /** Registers tasks created for a step as live, so cancellation can terminate them (§9.2). */
2925
+ trackLiveTasks(runId, taskIds) {
2926
+ if (!runId) return;
2927
+ const active = this.activeRuns.get(runId);
2928
+ if (!active) return;
2929
+ for (const id of taskIds) active.taskIds.add(id);
2688
2930
  }
2689
2931
  async executeStepOnce(step, ctx) {
2690
2932
  switch (step.kind) {
@@ -2717,7 +2959,9 @@ ${contribution}`);
2717
2959
  })
2718
2960
  )
2719
2961
  );
2962
+ this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
2720
2963
  const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
2964
+ const executionIds = results.map((r) => r.executions?.at(-1)?.executionId).filter((id) => id !== void 0);
2721
2965
  const completedTasks = results.filter((r) => r.status === "completed");
2722
2966
  const failedTasks = results.filter((r) => r.status !== "completed");
2723
2967
  const lines = results.map((result, index) => {
@@ -2740,7 +2984,13 @@ ${contribution}`);
2740
2984
  reports
2741
2985
  };
2742
2986
  }
2743
- return { status: "completed", summary, taskIds: created.map((t) => t.id), reports };
2987
+ return {
2988
+ status: "completed",
2989
+ summary,
2990
+ taskIds: created.map((t) => t.id),
2991
+ ...executionIds.length > 0 ? { executionIds } : {},
2992
+ reports
2993
+ };
2744
2994
  }
2745
2995
  async runSingle(step, ctx, agentName) {
2746
2996
  const task = await this.options.tasks.create({
@@ -2749,6 +2999,7 @@ ${contribution}`);
2749
2999
  ...ctx.role ? { role: ctx.role.id } : {},
2750
3000
  ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
2751
3001
  });
3002
+ this.trackLiveTasks(ctx.runId, [task.id]);
2752
3003
  const result = await this.options.tasks.run(task.id);
2753
3004
  if (result.status !== "completed") {
2754
3005
  return {
@@ -3226,7 +3477,7 @@ var ProjectStores = class {
3226
3477
  };
3227
3478
 
3228
3479
  // src/version.ts
3229
- var OMNINODE_VERSION = "2.0.0-alpha.4";
3480
+ var OMNINODE_VERSION = "2.0.0-alpha.6";
3230
3481
  export {
3231
3482
  AgentAdapterRegistry,
3232
3483
  AgentError,
@@ -3296,22 +3547,30 @@ export {
3296
3547
  encodeMessage,
3297
3548
  envelope,
3298
3549
  expandEnvRefs,
3550
+ extractMessage,
3299
3551
  extractReportFromText,
3300
3552
  findConfigFile,
3553
+ isNetworkError,
3301
3554
  isOmniNodeError,
3555
+ isRetryable,
3302
3556
  isSupportedProtocol,
3557
+ isTimeoutError,
3303
3558
  keywords,
3304
3559
  killTree,
3560
+ kindFromStatus,
3305
3561
  loadConfig,
3306
3562
  logger,
3307
3563
  newMessageId,
3308
3564
  parseHello,
3309
3565
  parsePlanJson,
3566
+ parseRetryAfter,
3310
3567
  pipelineConfigSchema,
3311
3568
  pipelineStepConfigSchema,
3312
3569
  plannerConfigSchema,
3313
3570
  projectConfigSchema,
3314
3571
  providerConfigSchema,
3572
+ providerHttpError,
3573
+ providerTransportError,
3315
3574
  registryKey,
3316
3575
  requestJson,
3317
3576
  requireCompletion,