@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/README.md +19 -4
- package/dist/cli/index.js +359 -82
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +143 -7
- package/dist/index.js +339 -80
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/version.ts
|
|
9
|
-
var OMNINODE_VERSION = "2.0.0-alpha.
|
|
9
|
+
var OMNINODE_VERSION = "2.0.0-alpha.6";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -363,35 +363,32 @@ function authHeaders(config, env = process.env) {
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
// src/providers/http.ts
|
|
366
|
+
var HttpResponseError = class extends Error {
|
|
367
|
+
constructor(message, status, cause) {
|
|
368
|
+
super(message, { cause });
|
|
369
|
+
this.status = status;
|
|
370
|
+
this.name = "HttpResponseError";
|
|
371
|
+
}
|
|
372
|
+
status;
|
|
373
|
+
};
|
|
366
374
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
367
375
|
async function requestJson(url, options = {}) {
|
|
368
376
|
const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
380
|
-
});
|
|
381
|
-
} catch (error) {
|
|
382
|
-
const reason = error instanceof Error && error.name === "TimeoutError" ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
|
|
383
|
-
throw new ProviderError("PROVIDER_UNAVAILABLE", `Request to ${url} failed: ${reason}.`, {
|
|
384
|
-
cause: error
|
|
385
|
-
});
|
|
386
|
-
}
|
|
377
|
+
const response = await fetch(url, {
|
|
378
|
+
method,
|
|
379
|
+
headers: {
|
|
380
|
+
accept: "application/json",
|
|
381
|
+
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
382
|
+
...headers
|
|
383
|
+
},
|
|
384
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
385
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
386
|
+
});
|
|
387
387
|
let parsed;
|
|
388
388
|
try {
|
|
389
389
|
parsed = await response.json();
|
|
390
390
|
} catch (error) {
|
|
391
|
-
throw new
|
|
392
|
-
cause: error,
|
|
393
|
-
details: { status: response.status }
|
|
394
|
-
});
|
|
391
|
+
throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
|
|
395
392
|
}
|
|
396
393
|
return { status: response.status, body: parsed };
|
|
397
394
|
}
|
|
@@ -452,23 +449,151 @@ var Logger = class _Logger {
|
|
|
452
449
|
};
|
|
453
450
|
var logger = new Logger();
|
|
454
451
|
|
|
452
|
+
// src/providers/errors.ts
|
|
453
|
+
function kindToCode(kind) {
|
|
454
|
+
switch (kind) {
|
|
455
|
+
case "AUTHENTICATION_ERROR":
|
|
456
|
+
return "PROVIDER_AUTH_FAILED";
|
|
457
|
+
case "MODEL_NOT_FOUND":
|
|
458
|
+
return "MODEL_NOT_FOUND";
|
|
459
|
+
default:
|
|
460
|
+
return "PROVIDER_UNAVAILABLE";
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function isRetryable(kind) {
|
|
464
|
+
return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
|
|
465
|
+
}
|
|
466
|
+
function kindFromStatus(status) {
|
|
467
|
+
if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
|
|
468
|
+
if (status === 429) return "RATE_LIMIT_ERROR";
|
|
469
|
+
if (status === 404) return "MODEL_NOT_FOUND";
|
|
470
|
+
if (status === 408 || status === 504) return "TIMEOUT";
|
|
471
|
+
if (status >= 500) return "SERVER_ERROR";
|
|
472
|
+
if (status >= 400) return "INVALID_REQUEST";
|
|
473
|
+
return "UNKNOWN_ERROR";
|
|
474
|
+
}
|
|
475
|
+
function extractMessage(body) {
|
|
476
|
+
if (body === null || typeof body !== "object") return void 0;
|
|
477
|
+
const root = body;
|
|
478
|
+
const error = root.error ?? root;
|
|
479
|
+
const message = error.message ?? error.detail ?? root.message;
|
|
480
|
+
return typeof message === "string" ? message : void 0;
|
|
481
|
+
}
|
|
482
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
483
|
+
if (!value) return void 0;
|
|
484
|
+
const seconds = Number(value);
|
|
485
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
486
|
+
const date = Date.parse(value);
|
|
487
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
|
|
488
|
+
}
|
|
489
|
+
function isTimeoutError(error) {
|
|
490
|
+
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
|
|
491
|
+
}
|
|
492
|
+
function fallbackMessage(kind, context, status) {
|
|
493
|
+
const { provider, operation } = context;
|
|
494
|
+
switch (kind) {
|
|
495
|
+
case "AUTHENTICATION_ERROR":
|
|
496
|
+
return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
|
|
497
|
+
case "RATE_LIMIT_ERROR":
|
|
498
|
+
return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
|
|
499
|
+
case "MODEL_NOT_FOUND":
|
|
500
|
+
return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
|
|
501
|
+
case "TIMEOUT":
|
|
502
|
+
return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
|
|
503
|
+
case "SERVER_ERROR":
|
|
504
|
+
return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
|
|
505
|
+
case "INVALID_REQUEST":
|
|
506
|
+
return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
|
|
507
|
+
default:
|
|
508
|
+
return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function providerHttpError(context) {
|
|
512
|
+
const status = context.status ?? 0;
|
|
513
|
+
const kind = kindFromStatus(status);
|
|
514
|
+
const detail = extractMessage(context.body);
|
|
515
|
+
const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
|
|
516
|
+
return new ProviderError(
|
|
517
|
+
context.code ?? kindToCode(kind),
|
|
518
|
+
detail ?? fallbackMessage(kind, context, status),
|
|
519
|
+
{
|
|
520
|
+
cause: context.cause,
|
|
521
|
+
details: {
|
|
522
|
+
kind,
|
|
523
|
+
provider: context.provider,
|
|
524
|
+
operation: context.operation,
|
|
525
|
+
status,
|
|
526
|
+
retryable: isRetryable(kind),
|
|
527
|
+
...retryAfterMs !== void 0 ? { retryAfterMs } : {},
|
|
528
|
+
...detail ? { providerMessage: detail } : {}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
function providerTransportError(context) {
|
|
534
|
+
const cause = context.cause;
|
|
535
|
+
const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
|
|
536
|
+
const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
|
|
537
|
+
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
|
|
538
|
+
cause: context.cause,
|
|
539
|
+
details: {
|
|
540
|
+
kind,
|
|
541
|
+
provider: context.provider,
|
|
542
|
+
operation: context.operation,
|
|
543
|
+
retryable: isRetryable(kind)
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
455
548
|
// src/providers/openai-compatible/index.ts
|
|
456
549
|
var OpenAICompatibleProvider = class {
|
|
457
550
|
config;
|
|
551
|
+
providerId;
|
|
552
|
+
authentication;
|
|
458
553
|
log;
|
|
459
|
-
|
|
554
|
+
/** Discovered models, so getModel() answers without another round-trip. */
|
|
555
|
+
discovered;
|
|
556
|
+
env;
|
|
557
|
+
constructor(config, log = logger, env = process.env) {
|
|
460
558
|
this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
|
|
559
|
+
this.env = env;
|
|
560
|
+
this.providerId = config.providerId ?? config.name;
|
|
561
|
+
this.authentication = {
|
|
562
|
+
method: config.apiKeyEnvVar ? "bearer" : "none",
|
|
563
|
+
...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
|
|
564
|
+
configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
|
|
565
|
+
};
|
|
461
566
|
this.log = log.child({ provider: config.name });
|
|
462
567
|
}
|
|
568
|
+
/** Authenticate + discover (roadmap §10, Model Discovery). */
|
|
569
|
+
async connect() {
|
|
570
|
+
authHeaders(this.config, this.env);
|
|
571
|
+
const models = await this.listModels();
|
|
572
|
+
this.discovered = models;
|
|
573
|
+
return models;
|
|
574
|
+
}
|
|
575
|
+
async getModel(modelId) {
|
|
576
|
+
const models = this.discovered ?? await this.connect();
|
|
577
|
+
return models.find((m) => m.id === modelId);
|
|
578
|
+
}
|
|
463
579
|
async listModels() {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
580
|
+
let response;
|
|
581
|
+
try {
|
|
582
|
+
response = await requestJson(`${this.config.baseUrl}/models`, {
|
|
583
|
+
headers: authHeaders(this.config, this.env)
|
|
584
|
+
});
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (error instanceof ProviderError) throw error;
|
|
587
|
+
throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
|
|
469
588
|
}
|
|
589
|
+
const { status, body } = response;
|
|
470
590
|
if (status !== 200) {
|
|
471
|
-
throw
|
|
591
|
+
throw providerHttpError({
|
|
592
|
+
provider: this.config.name,
|
|
593
|
+
operation: "listModels",
|
|
594
|
+
status,
|
|
595
|
+
body
|
|
596
|
+
});
|
|
472
597
|
}
|
|
473
598
|
const entries = extractModelEntries(body, this.config);
|
|
474
599
|
const models = [];
|
|
@@ -478,22 +603,40 @@ var OpenAICompatibleProvider = class {
|
|
|
478
603
|
skipped += 1;
|
|
479
604
|
continue;
|
|
480
605
|
}
|
|
481
|
-
models.push(
|
|
482
|
-
provider: this.config.name,
|
|
483
|
-
id: entry2.id,
|
|
484
|
-
displayName: entry2.id,
|
|
485
|
-
status: "available",
|
|
486
|
-
metadata: {
|
|
487
|
-
...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
|
|
488
|
-
...typeof entry2.created === "number" ? { createdAt: entry2.created } : {}
|
|
489
|
-
}
|
|
490
|
-
});
|
|
606
|
+
models.push(this.toModelInfo(entry2));
|
|
491
607
|
}
|
|
492
608
|
if (skipped > 0) {
|
|
493
609
|
this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
|
|
494
610
|
}
|
|
495
611
|
return models;
|
|
496
612
|
}
|
|
613
|
+
/** Maps a raw model entry onto the normalized metadata shape (§10). */
|
|
614
|
+
toModelInfo(entry2) {
|
|
615
|
+
const parameters = Array.isArray(entry2.supported_parameters) ? entry2.supported_parameters.filter((p) => typeof p === "string") : void 0;
|
|
616
|
+
const contextWindow = typeof entry2.context_length === "number" ? entry2.context_length : typeof entry2.context_window === "number" ? entry2.context_window : void 0;
|
|
617
|
+
const inputTypes = normalizeModalities(entry2.input_modalities);
|
|
618
|
+
const outputTypes = normalizeModalities(entry2.output_modalities);
|
|
619
|
+
return {
|
|
620
|
+
provider: this.config.name,
|
|
621
|
+
id: entry2.id,
|
|
622
|
+
displayName: entry2.id,
|
|
623
|
+
...typeof entry2.name === "string" ? { name: entry2.name } : {},
|
|
624
|
+
status: "available",
|
|
625
|
+
...contextWindow !== void 0 ? { contextWindow } : {},
|
|
626
|
+
...inputTypes !== void 0 ? { inputTypes } : {},
|
|
627
|
+
...outputTypes !== void 0 ? { outputTypes } : {},
|
|
628
|
+
...parameters !== void 0 ? {
|
|
629
|
+
supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
|
|
630
|
+
supportsStructuredOutput: parameters.includes("response_format"),
|
|
631
|
+
supportsStreaming: parameters.includes("stream")
|
|
632
|
+
} : {},
|
|
633
|
+
metadata: {
|
|
634
|
+
...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
|
|
635
|
+
...typeof entry2.created === "number" ? { createdAt: entry2.created } : {},
|
|
636
|
+
...parameters !== void 0 ? { supportedParameters: parameters } : {}
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
}
|
|
497
640
|
async healthCheck() {
|
|
498
641
|
const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
499
642
|
try {
|
|
@@ -523,27 +666,34 @@ var OpenAICompatibleProvider = class {
|
|
|
523
666
|
}
|
|
524
667
|
}
|
|
525
668
|
async chat(request) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
{ details: { status } }
|
|
543
|
-
);
|
|
669
|
+
let response;
|
|
670
|
+
try {
|
|
671
|
+
response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
|
|
672
|
+
method: "POST",
|
|
673
|
+
headers: authHeaders(this.config, this.env),
|
|
674
|
+
body: {
|
|
675
|
+
model: request.model,
|
|
676
|
+
messages: request.messages,
|
|
677
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
678
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
|
|
679
|
+
...request.stop !== void 0 ? { stop: request.stop } : {}
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
} catch (error) {
|
|
683
|
+
if (error instanceof ProviderError) throw error;
|
|
684
|
+
throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
|
|
544
685
|
}
|
|
686
|
+
const { status, body } = response;
|
|
545
687
|
if (status !== 200) {
|
|
546
|
-
throw
|
|
688
|
+
throw providerHttpError({
|
|
689
|
+
provider: this.config.name,
|
|
690
|
+
operation: "chat",
|
|
691
|
+
status,
|
|
692
|
+
body,
|
|
693
|
+
...status === 404 ? {
|
|
694
|
+
code: "MODEL_NOT_FOUND"
|
|
695
|
+
} : {}
|
|
696
|
+
});
|
|
547
697
|
}
|
|
548
698
|
const payload = body;
|
|
549
699
|
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
@@ -565,6 +715,13 @@ var OpenAICompatibleProvider = class {
|
|
|
565
715
|
};
|
|
566
716
|
}
|
|
567
717
|
};
|
|
718
|
+
function normalizeModalities(value) {
|
|
719
|
+
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
|
|
720
|
+
if (!list) return void 0;
|
|
721
|
+
const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
|
|
722
|
+
const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
|
|
723
|
+
return modalities.length > 0 ? modalities : void 0;
|
|
724
|
+
}
|
|
568
725
|
function normalizeBaseUrl(baseUrl) {
|
|
569
726
|
return baseUrl.replace(/\/+$/, "");
|
|
570
727
|
}
|
|
@@ -579,14 +736,6 @@ function extractModelEntries(body, config) {
|
|
|
579
736
|
{ details: { endpoint: "models" } }
|
|
580
737
|
);
|
|
581
738
|
}
|
|
582
|
-
function authFailed(config, status) {
|
|
583
|
-
const envHint = config.apiKeyEnvVar ? ` Check that $${config.apiKeyEnvVar} is set and valid.` : " The provider requires authentication; configure api_key_env_var for it.";
|
|
584
|
-
return new ProviderError(
|
|
585
|
-
"PROVIDER_AUTH_FAILED",
|
|
586
|
-
`Authentication with provider "${config.name}" failed (HTTP ${status}).${envHint}`,
|
|
587
|
-
{ details: { status } }
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
739
|
function unavailable(config, message, status) {
|
|
591
740
|
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
|
|
592
741
|
details: { status }
|
|
@@ -603,7 +752,7 @@ var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
|
|
|
603
752
|
* Run the full connect flow. Throws on authentication or transport failure
|
|
604
753
|
* (use healthCheck() for a non-throwing probe).
|
|
605
754
|
*/
|
|
606
|
-
async
|
|
755
|
+
async connectAndRegister(registry) {
|
|
607
756
|
const apiKey = resolveApiKey(this.config);
|
|
608
757
|
const discovered = await this.listModels();
|
|
609
758
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -827,7 +976,7 @@ async function testProvider(name, options = {}) {
|
|
|
827
976
|
if (options.connect && provider instanceof OmniHilbrasProvider) {
|
|
828
977
|
const registry = new ModelRegistry();
|
|
829
978
|
try {
|
|
830
|
-
const result = await provider.
|
|
979
|
+
const result = await provider.connectAndRegister(registry);
|
|
831
980
|
printProviderStatus(result.status);
|
|
832
981
|
console.log(` registered: ${result.registered}`);
|
|
833
982
|
} catch (error) {
|
|
@@ -1634,7 +1783,7 @@ function describeExit(code, signal) {
|
|
|
1634
1783
|
return `Process exited with code ${code}.`;
|
|
1635
1784
|
}
|
|
1636
1785
|
function sanitizeChunk(chunk) {
|
|
1637
|
-
return chunk.toString("utf8").
|
|
1786
|
+
return [...chunk.toString("utf8")].filter((char) => char.charCodeAt(0) !== 0).join("");
|
|
1638
1787
|
}
|
|
1639
1788
|
function normalizeReport(payload, taskId, agentName, index) {
|
|
1640
1789
|
if (payload === null || typeof payload !== "object") return void 0;
|
|
@@ -2068,6 +2217,11 @@ var TaskEngine = class {
|
|
|
2068
2217
|
if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
|
|
2069
2218
|
return current;
|
|
2070
2219
|
}
|
|
2220
|
+
const currentState = await this.options.store.get(current.id);
|
|
2221
|
+
if (currentState?.status === "cancelled") {
|
|
2222
|
+
current = currentState;
|
|
2223
|
+
break;
|
|
2224
|
+
}
|
|
2071
2225
|
const result = {
|
|
2072
2226
|
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
2073
2227
|
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
@@ -3193,9 +3347,23 @@ var PipelineEngine = class {
|
|
|
3193
3347
|
}
|
|
3194
3348
|
options;
|
|
3195
3349
|
log;
|
|
3350
|
+
/** Runs executing in THIS process — cancellation can terminate them directly. */
|
|
3351
|
+
activeRuns = /* @__PURE__ */ new Map();
|
|
3196
3352
|
get store() {
|
|
3197
3353
|
return this.options.store;
|
|
3198
3354
|
}
|
|
3355
|
+
/**
|
|
3356
|
+
* Saves the run while preserving fields that may be set by another process
|
|
3357
|
+
* (e.g. a cancellation request) — an in-memory snapshot must never clobber
|
|
3358
|
+
* externally requested cancellation (§9.2).
|
|
3359
|
+
*/
|
|
3360
|
+
async persist(run2) {
|
|
3361
|
+
const current = await this.options.store.get(run2.id);
|
|
3362
|
+
await this.options.store.save({
|
|
3363
|
+
...run2,
|
|
3364
|
+
...current?.cancellationRequested === true ? { cancellationRequested: true, cancellationRequestedAt: current.cancellationRequestedAt } : {}
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3199
3367
|
/** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
|
|
3200
3368
|
validate(def) {
|
|
3201
3369
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -3295,18 +3463,44 @@ var PipelineEngine = class {
|
|
|
3295
3463
|
);
|
|
3296
3464
|
}
|
|
3297
3465
|
}
|
|
3466
|
+
if (step.model) {
|
|
3467
|
+
const separator = step.model.indexOf(":");
|
|
3468
|
+
if (separator <= 0 || separator === step.model.length - 1) {
|
|
3469
|
+
throw new PipelineError(
|
|
3470
|
+
"PIPELINE_INVALID",
|
|
3471
|
+
`Step "${step.id}" has an invalid model reference "${step.model}" \u2014 use "provider:model-id".`
|
|
3472
|
+
);
|
|
3473
|
+
}
|
|
3474
|
+
const providerName = step.model.slice(0, separator);
|
|
3475
|
+
if (!this.options.providers.some((p) => p.name === providerName)) {
|
|
3476
|
+
throw new PipelineError(
|
|
3477
|
+
"PIPELINE_INVALID",
|
|
3478
|
+
`Step "${step.id}" uses model "${step.model}" but provider "${providerName}" is not configured.`
|
|
3479
|
+
);
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3298
3482
|
}
|
|
3299
3483
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3300
3484
|
const run2 = {
|
|
3301
3485
|
id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
|
|
3302
3486
|
pipelineId: def.id,
|
|
3487
|
+
attempt: 1,
|
|
3303
3488
|
objective,
|
|
3304
3489
|
status: "running",
|
|
3305
3490
|
startedAt: now,
|
|
3306
3491
|
stepRuns: []
|
|
3307
3492
|
};
|
|
3308
3493
|
await this.options.store.save(run2);
|
|
3309
|
-
|
|
3494
|
+
const liveTaskIds = /* @__PURE__ */ new Set();
|
|
3495
|
+
this.activeRuns.set(run2.id, {
|
|
3496
|
+
taskIds: liveTaskIds,
|
|
3497
|
+
cancelTasks: async () => {
|
|
3498
|
+
for (const taskId of liveTaskIds) {
|
|
3499
|
+
await this.options.tasks.cancel(taskId).catch(() => void 0);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
});
|
|
3503
|
+
this.log.info(`Running pipeline ${def.id} as ${run2.id} (attempt ${run2.attempt}).`);
|
|
3310
3504
|
await this.options.audit?.record({
|
|
3311
3505
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3312
3506
|
action: "pipeline.run.started",
|
|
@@ -3318,7 +3512,22 @@ var PipelineEngine = class {
|
|
|
3318
3512
|
const pending = new Set(def.steps.map((s) => s.id));
|
|
3319
3513
|
const contextParts = [];
|
|
3320
3514
|
const allReports = [];
|
|
3515
|
+
let cancelled = false;
|
|
3321
3516
|
while (pending.size > 0) {
|
|
3517
|
+
const persisted = await this.options.store.get(run2.id);
|
|
3518
|
+
if (persisted?.cancellationRequested === true) {
|
|
3519
|
+
cancelled = true;
|
|
3520
|
+
run2.cancellationRequested = true;
|
|
3521
|
+
run2.cancellationRequestedAt = persisted.cancellationRequestedAt;
|
|
3522
|
+
await this.activeRuns.get(run2.id)?.cancelTasks();
|
|
3523
|
+
for (const step of def.steps.filter((s) => pending.has(s.id))) {
|
|
3524
|
+
const stepRun = { stepId: step.id, status: "cancelled" };
|
|
3525
|
+
run2.stepRuns.push(stepRun);
|
|
3526
|
+
options.onStep?.(stepRun);
|
|
3527
|
+
}
|
|
3528
|
+
pending.clear();
|
|
3529
|
+
break;
|
|
3530
|
+
}
|
|
3322
3531
|
const ready = def.steps.filter(
|
|
3323
3532
|
(step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
|
|
3324
3533
|
);
|
|
@@ -3335,7 +3544,8 @@ var PipelineEngine = class {
|
|
|
3335
3544
|
...role ? { role } : {},
|
|
3336
3545
|
combinedContext: contextParts.join("\n\n"),
|
|
3337
3546
|
reports: [...allReports],
|
|
3338
|
-
pipelineRunId: run2.id
|
|
3547
|
+
pipelineRunId: run2.id,
|
|
3548
|
+
runId: run2.id
|
|
3339
3549
|
};
|
|
3340
3550
|
return this.executeStep(step, ctx);
|
|
3341
3551
|
})
|
|
@@ -3352,6 +3562,8 @@ ${contribution}`);
|
|
|
3352
3562
|
}
|
|
3353
3563
|
const stepRun = {
|
|
3354
3564
|
stepId: step.id,
|
|
3565
|
+
attempt: outcome.attempt ?? 1,
|
|
3566
|
+
...outcome.executionIds && outcome.executionIds.length > 0 ? { executionIds: outcome.executionIds } : {},
|
|
3355
3567
|
status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
|
|
3356
3568
|
...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
|
|
3357
3569
|
...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
|
|
@@ -3361,12 +3573,12 @@ ${contribution}`);
|
|
|
3361
3573
|
options.onStep?.(stepRun);
|
|
3362
3574
|
});
|
|
3363
3575
|
run2.reports = [...allReports];
|
|
3364
|
-
await this.
|
|
3576
|
+
await this.persist(run2);
|
|
3365
3577
|
}
|
|
3366
3578
|
const allOutcomes = [...outcomes.values()];
|
|
3367
|
-
const failed = allOutcomes.some((o) => o.status === "failed");
|
|
3368
|
-
const partial = !failed && allOutcomes.some((o) => o.status === "partial");
|
|
3369
|
-
const finalStatus = failed ? "failed" : partial ? "partial" : "completed";
|
|
3579
|
+
const failed = !cancelled && allOutcomes.some((o) => o.status === "failed");
|
|
3580
|
+
const partial = !cancelled && !failed && allOutcomes.some((o) => o.status === "partial");
|
|
3581
|
+
const finalStatus = cancelled ? "cancelled" : failed ? "failed" : partial ? "partial" : "completed";
|
|
3370
3582
|
const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
|
|
3371
3583
|
let resultSummary;
|
|
3372
3584
|
for (let i = def.steps.length - 1; i >= 0; i -= 1) {
|
|
@@ -3394,16 +3606,19 @@ ${contribution}`);
|
|
|
3394
3606
|
run2.reports = collected;
|
|
3395
3607
|
}
|
|
3396
3608
|
}
|
|
3609
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3610
|
+
this.activeRuns.delete(run2.id);
|
|
3397
3611
|
const finished = {
|
|
3398
3612
|
...run2,
|
|
3399
3613
|
status: finalStatus,
|
|
3400
|
-
finishedAt
|
|
3614
|
+
finishedAt,
|
|
3615
|
+
attempts: [...run2.attempts ?? [], { attempt: run2.attempt, startedAt: run2.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
|
|
3401
3616
|
...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
|
|
3402
3617
|
...combinedReportId !== void 0 ? { combinedReportId } : {},
|
|
3403
3618
|
...planId !== void 0 ? { planId } : {},
|
|
3404
3619
|
...resultSummary !== void 0 ? { resultSummary } : {}
|
|
3405
3620
|
};
|
|
3406
|
-
await this.
|
|
3621
|
+
await this.persist(finished);
|
|
3407
3622
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
3408
3623
|
await this.options.audit?.record({
|
|
3409
3624
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3413,6 +3628,23 @@ ${contribution}`);
|
|
|
3413
3628
|
});
|
|
3414
3629
|
return finished;
|
|
3415
3630
|
}
|
|
3631
|
+
/** First-class cancellation (§9.2): requests cancellation and terminates
|
|
3632
|
+
* tasks that are running in this process immediately. */
|
|
3633
|
+
async cancel(runId) {
|
|
3634
|
+
const run2 = await this.options.store.get(runId);
|
|
3635
|
+
if (!run2) {
|
|
3636
|
+
throw new PipelineError("PIPELINE_NOT_FOUND", `Pipeline run "${runId}" does not exist.`);
|
|
3637
|
+
}
|
|
3638
|
+
await this.options.store.save({
|
|
3639
|
+
...run2,
|
|
3640
|
+
cancellationRequested: true,
|
|
3641
|
+
cancellationRequestedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3642
|
+
});
|
|
3643
|
+
const active = this.activeRuns.get(runId);
|
|
3644
|
+
if (active) {
|
|
3645
|
+
await active.cancelTasks();
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3416
3648
|
async get(runId) {
|
|
3417
3649
|
return this.options.store.get(runId);
|
|
3418
3650
|
}
|
|
@@ -3436,9 +3668,16 @@ ${contribution}`);
|
|
|
3436
3668
|
this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
|
|
3437
3669
|
}
|
|
3438
3670
|
last = await this.executeStepOnce(step, ctx);
|
|
3439
|
-
if (last.status !== "failed") return last;
|
|
3671
|
+
if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
|
|
3440
3672
|
}
|
|
3441
|
-
return last;
|
|
3673
|
+
return { ...last, attempt: retries + 1 };
|
|
3674
|
+
}
|
|
3675
|
+
/** Registers tasks created for a step as live, so cancellation can terminate them (§9.2). */
|
|
3676
|
+
trackLiveTasks(runId, taskIds) {
|
|
3677
|
+
if (!runId) return;
|
|
3678
|
+
const active = this.activeRuns.get(runId);
|
|
3679
|
+
if (!active) return;
|
|
3680
|
+
for (const id of taskIds) active.taskIds.add(id);
|
|
3442
3681
|
}
|
|
3443
3682
|
async executeStepOnce(step, ctx) {
|
|
3444
3683
|
switch (step.kind) {
|
|
@@ -3471,7 +3710,9 @@ ${contribution}`);
|
|
|
3471
3710
|
})
|
|
3472
3711
|
)
|
|
3473
3712
|
);
|
|
3713
|
+
this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
|
|
3474
3714
|
const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
|
|
3715
|
+
const executionIds = results.map((r) => r.executions?.at(-1)?.executionId).filter((id) => id !== void 0);
|
|
3475
3716
|
const completedTasks = results.filter((r) => r.status === "completed");
|
|
3476
3717
|
const failedTasks = results.filter((r) => r.status !== "completed");
|
|
3477
3718
|
const lines = results.map((result, index) => {
|
|
@@ -3494,7 +3735,13 @@ ${contribution}`);
|
|
|
3494
3735
|
reports
|
|
3495
3736
|
};
|
|
3496
3737
|
}
|
|
3497
|
-
return {
|
|
3738
|
+
return {
|
|
3739
|
+
status: "completed",
|
|
3740
|
+
summary,
|
|
3741
|
+
taskIds: created.map((t) => t.id),
|
|
3742
|
+
...executionIds.length > 0 ? { executionIds } : {},
|
|
3743
|
+
reports
|
|
3744
|
+
};
|
|
3498
3745
|
}
|
|
3499
3746
|
async runSingle(step, ctx, agentName) {
|
|
3500
3747
|
const task = await this.options.tasks.create({
|
|
@@ -3503,6 +3750,7 @@ ${contribution}`);
|
|
|
3503
3750
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
3504
3751
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
3505
3752
|
});
|
|
3753
|
+
this.trackLiveTasks(ctx.runId, [task.id]);
|
|
3506
3754
|
const result = await this.options.tasks.run(task.id);
|
|
3507
3755
|
if (result.status !== "completed") {
|
|
3508
3756
|
return {
|
|
@@ -3753,6 +4001,24 @@ function registerPipelineCommands(program) {
|
|
|
3753
4001
|
}
|
|
3754
4002
|
await runPipelineAction(def.id, objective ?? previous.objective);
|
|
3755
4003
|
});
|
|
4004
|
+
pipeline.command("cancel <run-id>").description("Request cancellation of a pipeline run (stops scheduling and cancels running tasks).").action(async (runId) => {
|
|
4005
|
+
const config = loadConfig();
|
|
4006
|
+
const engine = buildPipelineEngine(config);
|
|
4007
|
+
await engine.cancel(runId);
|
|
4008
|
+
console.log(`Cancellation requested for run ${runId}.`);
|
|
4009
|
+
console.log("A run executing in another process stops at its next check point.");
|
|
4010
|
+
});
|
|
4011
|
+
pipeline.command("validate <id>").description("Validate a pipeline definition before running it (\xA79.5).").action(async (id) => {
|
|
4012
|
+
const config = loadConfig();
|
|
4013
|
+
const def = findPipeline(config, id);
|
|
4014
|
+
const engine = buildPipelineEngine(config);
|
|
4015
|
+
engine.validate(def);
|
|
4016
|
+
console.log(`Pipeline "${def.id}" is valid (${def.steps.length} step(s)).`);
|
|
4017
|
+
for (const step of def.steps) {
|
|
4018
|
+
const deps = step.dependsOn?.join(", ") ?? "(previous step)";
|
|
4019
|
+
console.log(` ${step.id} kind=${step.kind} depends_on=${deps}`);
|
|
4020
|
+
}
|
|
4021
|
+
});
|
|
3756
4022
|
pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
|
|
3757
4023
|
const config = loadConfig();
|
|
3758
4024
|
const engine = buildPipelineEngine(config);
|
|
@@ -3762,7 +4028,8 @@ function registerPipelineCommands(program) {
|
|
|
3762
4028
|
return;
|
|
3763
4029
|
}
|
|
3764
4030
|
for (const run2 of runs) {
|
|
3765
|
-
|
|
4031
|
+
const note = isInterrupted(run2) ? " (interrupted \u2014 process gone before finish)" : "";
|
|
4032
|
+
console.log(`${run2.id} ${run2.status.padEnd(10)} ${run2.startedAt ?? "?"}${note}`);
|
|
3766
4033
|
}
|
|
3767
4034
|
});
|
|
3768
4035
|
pipeline.command("status <run-id>").description("Show the recorded state of a pipeline run.").action(async (runId) => {
|
|
@@ -3780,6 +4047,13 @@ function registerPipelineCommands(program) {
|
|
|
3780
4047
|
console.log(` status: ${run2.status}`);
|
|
3781
4048
|
console.log(` started: ${run2.startedAt ?? "?"}`);
|
|
3782
4049
|
if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
|
|
4050
|
+
console.log(` attempt: ${run2.attempt}`);
|
|
4051
|
+
if (run2.cancellationRequested) {
|
|
4052
|
+
console.log(` cancellation requested at ${run2.cancellationRequestedAt ?? "?"}`);
|
|
4053
|
+
}
|
|
4054
|
+
if (isInterrupted(run2)) {
|
|
4055
|
+
console.log(" note: this run looks interrupted (no finishedAt) \u2014 retry with `pipeline retry`");
|
|
4056
|
+
}
|
|
3783
4057
|
if (run2.resultSummary) console.log(` result: ${run2.resultSummary}`);
|
|
3784
4058
|
for (const stepRun of run2.stepRuns) {
|
|
3785
4059
|
console.log(` [${stepRun.stepId}] ${stepRun.status}`);
|
|
@@ -3821,6 +4095,9 @@ async function runPipelineAction(id, objective) {
|
|
|
3821
4095
|
function registerRunAlias(program) {
|
|
3822
4096
|
program.command("run <id> [objective]").description("Alias of `omninode pipeline run` \u2014 execute a configured pipeline end to end.").action(async (id, objective) => runPipelineAction(id, objective));
|
|
3823
4097
|
}
|
|
4098
|
+
function isInterrupted(run2) {
|
|
4099
|
+
return run2.status === "running" && run2.finishedAt === void 0;
|
|
4100
|
+
}
|
|
3824
4101
|
function findPipeline(config, id) {
|
|
3825
4102
|
const def = config.project.pipelines.find((p) => p.id === id);
|
|
3826
4103
|
if (!def) {
|