@hilbras/omninode 1.0.0 → 2.0.0-alpha.10
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 +70 -1
- package/dist/cli/index.js +1696 -512
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1037 -129
- package/dist/index.js +1717 -486
- 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 = "
|
|
9
|
+
var OMNINODE_VERSION = "2.0.0-alpha.10";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -44,11 +44,6 @@ var PipelineError = class extends OmniNodeError {
|
|
|
44
44
|
super(code, message, options);
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
|
-
var ProtocolError = class extends OmniNodeError {
|
|
48
|
-
constructor(message, options = {}) {
|
|
49
|
-
super("PROTOCOL_VIOLATION", message, options);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
47
|
var PlannerError = class extends OmniNodeError {
|
|
53
48
|
constructor(code, message, options = {}) {
|
|
54
49
|
super(code, message, options);
|
|
@@ -73,6 +68,7 @@ var providerConfigSchema = z.object({
|
|
|
73
68
|
type: z.enum(["openai-compatible", "omnihilbras", "openrouter", "local", "custom"]),
|
|
74
69
|
base_url: z.string().url(),
|
|
75
70
|
api_key_env_var: z.string().min(1).optional(),
|
|
71
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
76
72
|
headers: z.record(z.string()).optional(),
|
|
77
73
|
enabled: z.boolean().optional(),
|
|
78
74
|
metadata: z.record(z.unknown()).optional()
|
|
@@ -88,6 +84,11 @@ var agentConfigSchema = z.object({
|
|
|
88
84
|
input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
|
|
89
85
|
timeout_ms: z.number().int().positive().optional(),
|
|
90
86
|
inherit_env: z.boolean().optional(),
|
|
87
|
+
env_policy: z.enum(["inherit", "allowlist", "denylist", "explicit"]).optional(),
|
|
88
|
+
env_allowlist: z.array(z.string()).optional(),
|
|
89
|
+
env_denylist: z.array(z.string()).optional(),
|
|
90
|
+
max_output_bytes: z.number().int().positive().optional(),
|
|
91
|
+
allow_external_cwd: z.boolean().optional(),
|
|
91
92
|
metadata: z.record(z.unknown()).optional()
|
|
92
93
|
}).strict();
|
|
93
94
|
var roleConfigSchema = z.object({
|
|
@@ -132,7 +133,9 @@ var projectConfigSchema = z.object({
|
|
|
132
133
|
memory: z.object({
|
|
133
134
|
provider: z.string().min(1).default("local"),
|
|
134
135
|
base_url: z.string().url().optional(),
|
|
135
|
-
api_key_env_var: z.string().min(1).optional()
|
|
136
|
+
api_key_env_var: z.string().min(1).optional(),
|
|
137
|
+
/** When true, memory failures fail tasks instead of being tolerated (§12). */
|
|
138
|
+
required: z.boolean().optional()
|
|
136
139
|
}).strict().optional()
|
|
137
140
|
}).strict();
|
|
138
141
|
var appConfigSchema = z.object({
|
|
@@ -214,7 +217,8 @@ function toAppConfig(yaml) {
|
|
|
214
217
|
memory: {
|
|
215
218
|
provider: yaml.project.memory.provider,
|
|
216
219
|
baseUrl: yaml.project.memory.base_url,
|
|
217
|
-
apiKeyEnvVar: yaml.project.memory.api_key_env_var
|
|
220
|
+
apiKeyEnvVar: yaml.project.memory.api_key_env_var,
|
|
221
|
+
required: yaml.project.memory.required
|
|
218
222
|
}
|
|
219
223
|
} : {}
|
|
220
224
|
},
|
|
@@ -245,6 +249,7 @@ function toProviderConfig(raw) {
|
|
|
245
249
|
type: raw.type,
|
|
246
250
|
baseUrl: raw.base_url,
|
|
247
251
|
apiKeyEnvVar: raw.api_key_env_var,
|
|
252
|
+
timeoutMs: raw.timeout_ms,
|
|
248
253
|
headers: raw.headers,
|
|
249
254
|
enabled: raw.enabled,
|
|
250
255
|
metadata: raw.metadata
|
|
@@ -262,6 +267,11 @@ function toAgentConfig(raw) {
|
|
|
262
267
|
inputMode: raw.input_mode,
|
|
263
268
|
timeoutMs: raw.timeout_ms,
|
|
264
269
|
inheritEnv: raw.inherit_env,
|
|
270
|
+
envPolicy: raw.env_policy,
|
|
271
|
+
envAllowlist: raw.env_allowlist,
|
|
272
|
+
envDenylist: raw.env_denylist,
|
|
273
|
+
maxOutputBytes: raw.max_output_bytes,
|
|
274
|
+
allowExternalCwd: raw.allow_external_cwd,
|
|
265
275
|
metadata: raw.metadata
|
|
266
276
|
};
|
|
267
277
|
}
|
|
@@ -358,35 +368,32 @@ function authHeaders(config, env = process.env) {
|
|
|
358
368
|
}
|
|
359
369
|
|
|
360
370
|
// src/providers/http.ts
|
|
371
|
+
var HttpResponseError = class extends Error {
|
|
372
|
+
constructor(message, status, cause) {
|
|
373
|
+
super(message, { cause });
|
|
374
|
+
this.status = status;
|
|
375
|
+
this.name = "HttpResponseError";
|
|
376
|
+
}
|
|
377
|
+
status;
|
|
378
|
+
};
|
|
361
379
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
362
380
|
async function requestJson(url, options = {}) {
|
|
363
381
|
const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
375
|
-
});
|
|
376
|
-
} catch (error) {
|
|
377
|
-
const reason = error instanceof Error && error.name === "TimeoutError" ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
|
|
378
|
-
throw new ProviderError("PROVIDER_UNAVAILABLE", `Request to ${url} failed: ${reason}.`, {
|
|
379
|
-
cause: error
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
+
const response = await fetch(url, {
|
|
383
|
+
method,
|
|
384
|
+
headers: {
|
|
385
|
+
accept: "application/json",
|
|
386
|
+
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
387
|
+
...headers
|
|
388
|
+
},
|
|
389
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
390
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
391
|
+
});
|
|
382
392
|
let parsed;
|
|
383
393
|
try {
|
|
384
394
|
parsed = await response.json();
|
|
385
395
|
} catch (error) {
|
|
386
|
-
throw new
|
|
387
|
-
cause: error,
|
|
388
|
-
details: { status: response.status }
|
|
389
|
-
});
|
|
396
|
+
throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
|
|
390
397
|
}
|
|
391
398
|
return { status: response.status, body: parsed };
|
|
392
399
|
}
|
|
@@ -447,24 +454,167 @@ var Logger = class _Logger {
|
|
|
447
454
|
};
|
|
448
455
|
var logger = new Logger();
|
|
449
456
|
|
|
457
|
+
// src/providers/errors.ts
|
|
458
|
+
function kindToCode(kind) {
|
|
459
|
+
switch (kind) {
|
|
460
|
+
case "AUTHENTICATION_ERROR":
|
|
461
|
+
return "PROVIDER_AUTH_FAILED";
|
|
462
|
+
case "MODEL_NOT_FOUND":
|
|
463
|
+
return "MODEL_NOT_FOUND";
|
|
464
|
+
default:
|
|
465
|
+
return "PROVIDER_UNAVAILABLE";
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function isRetryable(kind) {
|
|
469
|
+
return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
|
|
470
|
+
}
|
|
471
|
+
function kindFromStatus(status) {
|
|
472
|
+
if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
|
|
473
|
+
if (status === 429) return "RATE_LIMIT_ERROR";
|
|
474
|
+
if (status === 404) return "MODEL_NOT_FOUND";
|
|
475
|
+
if (status === 408 || status === 504) return "TIMEOUT";
|
|
476
|
+
if (status >= 500) return "SERVER_ERROR";
|
|
477
|
+
if (status >= 400) return "INVALID_REQUEST";
|
|
478
|
+
return "UNKNOWN_ERROR";
|
|
479
|
+
}
|
|
480
|
+
function extractMessage(body) {
|
|
481
|
+
if (body === null || typeof body !== "object") return void 0;
|
|
482
|
+
const root = body;
|
|
483
|
+
const error = root.error ?? root;
|
|
484
|
+
const message = error.message ?? error.detail ?? root.message;
|
|
485
|
+
return typeof message === "string" ? message : void 0;
|
|
486
|
+
}
|
|
487
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
488
|
+
if (!value) return void 0;
|
|
489
|
+
const seconds = Number(value);
|
|
490
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
491
|
+
const date = Date.parse(value);
|
|
492
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
|
|
493
|
+
}
|
|
494
|
+
function isTimeoutError(error) {
|
|
495
|
+
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
|
|
496
|
+
}
|
|
497
|
+
function fallbackMessage(kind, context, status) {
|
|
498
|
+
const { provider, operation } = context;
|
|
499
|
+
switch (kind) {
|
|
500
|
+
case "AUTHENTICATION_ERROR":
|
|
501
|
+
return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
|
|
502
|
+
case "RATE_LIMIT_ERROR":
|
|
503
|
+
return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
|
|
504
|
+
case "MODEL_NOT_FOUND":
|
|
505
|
+
return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
|
|
506
|
+
case "TIMEOUT":
|
|
507
|
+
return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
|
|
508
|
+
case "SERVER_ERROR":
|
|
509
|
+
return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
|
|
510
|
+
case "INVALID_REQUEST":
|
|
511
|
+
return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
|
|
512
|
+
default:
|
|
513
|
+
return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function providerHttpError(context) {
|
|
517
|
+
const status = context.status ?? 0;
|
|
518
|
+
const kind = kindFromStatus(status);
|
|
519
|
+
const detail = extractMessage(context.body);
|
|
520
|
+
const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
|
|
521
|
+
return new ProviderError(
|
|
522
|
+
context.code ?? kindToCode(kind),
|
|
523
|
+
detail ?? fallbackMessage(kind, context, status),
|
|
524
|
+
{
|
|
525
|
+
cause: context.cause,
|
|
526
|
+
details: {
|
|
527
|
+
kind,
|
|
528
|
+
provider: context.provider,
|
|
529
|
+
operation: context.operation,
|
|
530
|
+
status,
|
|
531
|
+
retryable: isRetryable(kind),
|
|
532
|
+
...retryAfterMs !== void 0 ? { retryAfterMs } : {},
|
|
533
|
+
...detail ? { providerMessage: detail } : {}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
function providerTransportError(context) {
|
|
539
|
+
const cause = context.cause;
|
|
540
|
+
const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
|
|
541
|
+
const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
|
|
542
|
+
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
|
|
543
|
+
cause: context.cause,
|
|
544
|
+
details: {
|
|
545
|
+
kind,
|
|
546
|
+
provider: context.provider,
|
|
547
|
+
operation: context.operation,
|
|
548
|
+
retryable: isRetryable(kind)
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
450
553
|
// src/providers/openai-compatible/index.ts
|
|
554
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
451
555
|
var OpenAICompatibleProvider = class {
|
|
452
556
|
config;
|
|
557
|
+
providerId;
|
|
558
|
+
authentication;
|
|
453
559
|
log;
|
|
454
|
-
|
|
560
|
+
/** Discovered models, so getModel() answers without another round-trip. */
|
|
561
|
+
discovered;
|
|
562
|
+
env;
|
|
563
|
+
constructor(config, log = logger, env = process.env) {
|
|
455
564
|
this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
|
|
565
|
+
this.env = env;
|
|
566
|
+
this.providerId = config.providerId ?? config.name;
|
|
567
|
+
this.authentication = {
|
|
568
|
+
method: config.apiKeyEnvVar ? "bearer" : "none",
|
|
569
|
+
...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
|
|
570
|
+
configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
|
|
571
|
+
};
|
|
456
572
|
this.log = log.child({ provider: config.name });
|
|
457
573
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
574
|
+
/** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
|
|
575
|
+
get requestTimeoutMs() {
|
|
576
|
+
return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
577
|
+
}
|
|
578
|
+
/** Authenticate + discover (roadmap §10, Model Discovery). */
|
|
579
|
+
async connect() {
|
|
580
|
+
authHeaders(this.config, this.env);
|
|
581
|
+
const models = await this.listModels();
|
|
582
|
+
this.discovered = models;
|
|
583
|
+
return models;
|
|
584
|
+
}
|
|
585
|
+
async getModel(modelId) {
|
|
586
|
+
const models = this.discovered ?? await this.connect();
|
|
587
|
+
return models.find((m) => m.id === modelId);
|
|
588
|
+
}
|
|
589
|
+
/** Raw /models body (protected so adapters can capture gateway-level metadata). */
|
|
590
|
+
async rawModelsResponse() {
|
|
591
|
+
const response = await this.modelsRequest();
|
|
592
|
+
return response.body;
|
|
593
|
+
}
|
|
594
|
+
async modelsRequest() {
|
|
595
|
+
let response;
|
|
596
|
+
try {
|
|
597
|
+
response = await requestJson(`${this.config.baseUrl}/models`, {
|
|
598
|
+
headers: authHeaders(this.config, this.env),
|
|
599
|
+
timeoutMs: this.requestTimeoutMs
|
|
600
|
+
});
|
|
601
|
+
} catch (error) {
|
|
602
|
+
if (error instanceof ProviderError) throw error;
|
|
603
|
+
throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
|
|
464
604
|
}
|
|
605
|
+
const { status, body } = response;
|
|
465
606
|
if (status !== 200) {
|
|
466
|
-
throw
|
|
607
|
+
throw providerHttpError({
|
|
608
|
+
provider: this.config.name,
|
|
609
|
+
operation: "listModels",
|
|
610
|
+
status,
|
|
611
|
+
body
|
|
612
|
+
});
|
|
467
613
|
}
|
|
614
|
+
return response;
|
|
615
|
+
}
|
|
616
|
+
async listModels() {
|
|
617
|
+
const body = await this.rawModelsResponse();
|
|
468
618
|
const entries = extractModelEntries(body, this.config);
|
|
469
619
|
const models = [];
|
|
470
620
|
let skipped = 0;
|
|
@@ -473,22 +623,44 @@ var OpenAICompatibleProvider = class {
|
|
|
473
623
|
skipped += 1;
|
|
474
624
|
continue;
|
|
475
625
|
}
|
|
476
|
-
models.push(
|
|
477
|
-
provider: this.config.name,
|
|
478
|
-
id: entry2.id,
|
|
479
|
-
displayName: entry2.id,
|
|
480
|
-
status: "available",
|
|
481
|
-
metadata: {
|
|
482
|
-
...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
|
|
483
|
-
...typeof entry2.created === "number" ? { createdAt: entry2.created } : {}
|
|
484
|
-
}
|
|
485
|
-
});
|
|
626
|
+
models.push(this.toModelInfo(entry2));
|
|
486
627
|
}
|
|
487
628
|
if (skipped > 0) {
|
|
488
629
|
this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
|
|
489
630
|
}
|
|
490
631
|
return models;
|
|
491
632
|
}
|
|
633
|
+
/** Maps a raw model entry onto the normalized metadata shape (§10). */
|
|
634
|
+
toModelInfo(entry2) {
|
|
635
|
+
const parameters = Array.isArray(entry2.supported_parameters) ? entry2.supported_parameters.filter((p) => typeof p === "string") : void 0;
|
|
636
|
+
const contextWindow = typeof entry2.context_length === "number" ? entry2.context_length : typeof entry2.context_window === "number" ? entry2.context_window : void 0;
|
|
637
|
+
const inputTypes = normalizeModalities(entry2.input_modalities);
|
|
638
|
+
const outputTypes = normalizeModalities(entry2.output_modalities);
|
|
639
|
+
return {
|
|
640
|
+
provider: this.config.name,
|
|
641
|
+
id: entry2.id,
|
|
642
|
+
displayName: entry2.id,
|
|
643
|
+
...typeof entry2.name === "string" ? { name: entry2.name } : {},
|
|
644
|
+
status: "available",
|
|
645
|
+
...contextWindow !== void 0 ? { contextWindow } : {},
|
|
646
|
+
...inputTypes !== void 0 ? { inputTypes } : {},
|
|
647
|
+
...outputTypes !== void 0 ? { outputTypes } : {},
|
|
648
|
+
...parameters !== void 0 ? {
|
|
649
|
+
supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
|
|
650
|
+
supportsStructuredOutput: parameters.includes("response_format"),
|
|
651
|
+
supportsStreaming: parameters.includes("stream")
|
|
652
|
+
} : {},
|
|
653
|
+
metadata: {
|
|
654
|
+
...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
|
|
655
|
+
...typeof entry2.created === "number" ? { createdAt: entry2.created } : {},
|
|
656
|
+
...parameters !== void 0 ? { supportedParameters: parameters } : {}
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
/** Streaming where the gateway supports it (§11). */
|
|
661
|
+
stream(request, onChunk) {
|
|
662
|
+
return streamChatCompletion(this.config, request, onChunk, this.env);
|
|
663
|
+
}
|
|
492
664
|
async healthCheck() {
|
|
493
665
|
const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
494
666
|
try {
|
|
@@ -518,27 +690,35 @@ var OpenAICompatibleProvider = class {
|
|
|
518
690
|
}
|
|
519
691
|
}
|
|
520
692
|
async chat(request) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
);
|
|
693
|
+
let response;
|
|
694
|
+
try {
|
|
695
|
+
response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
|
|
696
|
+
method: "POST",
|
|
697
|
+
headers: authHeaders(this.config, this.env),
|
|
698
|
+
timeoutMs: this.requestTimeoutMs,
|
|
699
|
+
body: {
|
|
700
|
+
model: request.model,
|
|
701
|
+
messages: request.messages,
|
|
702
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
703
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
|
|
704
|
+
...request.stop !== void 0 ? { stop: request.stop } : {}
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
} catch (error) {
|
|
708
|
+
if (error instanceof ProviderError) throw error;
|
|
709
|
+
throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
|
|
539
710
|
}
|
|
711
|
+
const { status, body } = response;
|
|
540
712
|
if (status !== 200) {
|
|
541
|
-
throw
|
|
713
|
+
throw providerHttpError({
|
|
714
|
+
provider: this.config.name,
|
|
715
|
+
operation: "chat",
|
|
716
|
+
status,
|
|
717
|
+
body,
|
|
718
|
+
...status === 404 ? {
|
|
719
|
+
code: "MODEL_NOT_FOUND"
|
|
720
|
+
} : {}
|
|
721
|
+
});
|
|
542
722
|
}
|
|
543
723
|
const payload = body;
|
|
544
724
|
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
@@ -560,6 +740,81 @@ var OpenAICompatibleProvider = class {
|
|
|
560
740
|
};
|
|
561
741
|
}
|
|
562
742
|
};
|
|
743
|
+
async function streamChatCompletion(config, request, onChunk, env = process.env) {
|
|
744
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
745
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
746
|
+
let response;
|
|
747
|
+
try {
|
|
748
|
+
response = await fetch(`${baseUrl}/chat/completions`, {
|
|
749
|
+
method: "POST",
|
|
750
|
+
headers: {
|
|
751
|
+
accept: "text/event-stream",
|
|
752
|
+
"content-type": "application/json",
|
|
753
|
+
...authHeaders(config, env)
|
|
754
|
+
},
|
|
755
|
+
body: JSON.stringify({
|
|
756
|
+
model: request.model,
|
|
757
|
+
messages: request.messages,
|
|
758
|
+
stream: true,
|
|
759
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
760
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
|
|
761
|
+
}),
|
|
762
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
763
|
+
});
|
|
764
|
+
} catch (error) {
|
|
765
|
+
throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
|
|
766
|
+
}
|
|
767
|
+
if (!response.ok) {
|
|
768
|
+
const body = await response.json().catch(() => void 0);
|
|
769
|
+
throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
|
|
770
|
+
}
|
|
771
|
+
if (!response.body) {
|
|
772
|
+
throw providerHttpError({
|
|
773
|
+
provider: config.name,
|
|
774
|
+
operation: "stream",
|
|
775
|
+
status: 500,
|
|
776
|
+
body: { error: { message: "streaming is not supported by this endpoint" } }
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
let content = "";
|
|
780
|
+
let model = request.model;
|
|
781
|
+
let finishReason;
|
|
782
|
+
const decoder = new TextDecoder();
|
|
783
|
+
let buffer = "";
|
|
784
|
+
for await (const piece of response.body) {
|
|
785
|
+
buffer += decoder.decode(piece, { stream: true });
|
|
786
|
+
let newline = buffer.indexOf("\n");
|
|
787
|
+
while (newline >= 0) {
|
|
788
|
+
const line = buffer.slice(0, newline).trim();
|
|
789
|
+
buffer = buffer.slice(newline + 1);
|
|
790
|
+
newline = buffer.indexOf("\n");
|
|
791
|
+
if (line.length === 0 || line.startsWith(":")) continue;
|
|
792
|
+
const data = line.startsWith("data:") ? line.slice(5).trim() : line;
|
|
793
|
+
if (data === "[DONE]") continue;
|
|
794
|
+
let event;
|
|
795
|
+
try {
|
|
796
|
+
event = JSON.parse(data);
|
|
797
|
+
} catch {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (typeof event.model === "string") model = event.model;
|
|
801
|
+
const choices = Array.isArray(event.choices) ? event.choices : [];
|
|
802
|
+
const first = choices[0];
|
|
803
|
+
const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
|
|
804
|
+
if (delta.length > 0) content += delta;
|
|
805
|
+
if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
|
|
806
|
+
onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
|
|
810
|
+
}
|
|
811
|
+
function normalizeModalities(value) {
|
|
812
|
+
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
|
|
813
|
+
if (!list) return void 0;
|
|
814
|
+
const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
|
|
815
|
+
const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
|
|
816
|
+
return modalities.length > 0 ? modalities : void 0;
|
|
817
|
+
}
|
|
563
818
|
function normalizeBaseUrl(baseUrl) {
|
|
564
819
|
return baseUrl.replace(/\/+$/, "");
|
|
565
820
|
}
|
|
@@ -574,14 +829,6 @@ function extractModelEntries(body, config) {
|
|
|
574
829
|
{ details: { endpoint: "models" } }
|
|
575
830
|
);
|
|
576
831
|
}
|
|
577
|
-
function authFailed(config, status) {
|
|
578
|
-
const envHint = config.apiKeyEnvVar ? ` Check that $${config.apiKeyEnvVar} is set and valid.` : " The provider requires authentication; configure api_key_env_var for it.";
|
|
579
|
-
return new ProviderError(
|
|
580
|
-
"PROVIDER_AUTH_FAILED",
|
|
581
|
-
`Authentication with provider "${config.name}" failed (HTTP ${status}).${envHint}`,
|
|
582
|
-
{ details: { status } }
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
832
|
function unavailable(config, message, status) {
|
|
586
833
|
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
|
|
587
834
|
details: { status }
|
|
@@ -594,11 +841,33 @@ function inferHealth(error) {
|
|
|
594
841
|
|
|
595
842
|
// src/providers/omnihilbras/index.ts
|
|
596
843
|
var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
|
|
844
|
+
gateway;
|
|
845
|
+
async listModels() {
|
|
846
|
+
const raw = await this.rawModelsResponse();
|
|
847
|
+
await this.extractGatewayMetadata(raw);
|
|
848
|
+
const models = await super.listModels();
|
|
849
|
+
return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
|
|
850
|
+
}
|
|
597
851
|
/**
|
|
598
852
|
* Run the full connect flow. Throws on authentication or transport failure
|
|
599
853
|
* (use healthCheck() for a non-throwing probe).
|
|
600
854
|
*/
|
|
601
|
-
|
|
855
|
+
/**
|
|
856
|
+
* Gateway metadata reported alongside the model list (gateway version, tier,
|
|
857
|
+
* region…), kept separate from per-model metadata (§11 — provider metadata).
|
|
858
|
+
*/
|
|
859
|
+
gatewayMetadata() {
|
|
860
|
+
return this.gateway;
|
|
861
|
+
}
|
|
862
|
+
async extractGatewayMetadata(body) {
|
|
863
|
+
if (body !== null && typeof body === "object") {
|
|
864
|
+
const gateway = body.gateway;
|
|
865
|
+
if (gateway !== null && typeof gateway === "object") {
|
|
866
|
+
this.gateway = gateway;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
async connectAndRegister(registry) {
|
|
602
871
|
const apiKey = resolveApiKey(this.config);
|
|
603
872
|
const discovered = await this.listModels();
|
|
604
873
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -822,7 +1091,7 @@ async function testProvider(name, options = {}) {
|
|
|
822
1091
|
if (options.connect && provider instanceof OmniHilbrasProvider) {
|
|
823
1092
|
const registry = new ModelRegistry();
|
|
824
1093
|
try {
|
|
825
|
-
const result = await provider.
|
|
1094
|
+
const result = await provider.connectAndRegister(registry);
|
|
826
1095
|
printProviderStatus(result.status);
|
|
827
1096
|
console.log(` registered: ${result.registered}`);
|
|
828
1097
|
} catch (error) {
|
|
@@ -897,19 +1166,416 @@ function registerModelsCommand(program) {
|
|
|
897
1166
|
|
|
898
1167
|
// src/agents/process/index.ts
|
|
899
1168
|
import { spawn } from "child_process";
|
|
900
|
-
|
|
901
|
-
|
|
1169
|
+
|
|
1170
|
+
// src/agent-protocol/version.ts
|
|
1171
|
+
var PROTOCOL_V2 = "omninode-agent-protocol/2";
|
|
1172
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
1173
|
+
"omninode-agent-protocol/1",
|
|
1174
|
+
PROTOCOL_V2
|
|
1175
|
+
];
|
|
1176
|
+
var MAX_MESSAGE_BYTES = 1048576;
|
|
1177
|
+
function isSupportedProtocol(protocol) {
|
|
1178
|
+
return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// src/agent-protocol/types.ts
|
|
1182
|
+
function newMessageId(prefix = "msg") {
|
|
1183
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
1184
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1185
|
+
}
|
|
1186
|
+
function envelope(type, payload, ids = {}) {
|
|
1187
|
+
return {
|
|
1188
|
+
protocol: PROTOCOL_V2,
|
|
1189
|
+
messageId: newMessageId(),
|
|
1190
|
+
...ids.requestId !== void 0 ? { requestId: ids.requestId } : {},
|
|
1191
|
+
...ids.taskId !== void 0 ? { taskId: ids.taskId } : {},
|
|
1192
|
+
...ids.agentId !== void 0 ? { agentId: ids.agentId } : {},
|
|
1193
|
+
...ids.pipelineId !== void 0 ? { pipelineId: ids.pipelineId } : {},
|
|
1194
|
+
...ids.executionId !== void 0 ? { executionId: ids.executionId } : {},
|
|
1195
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1196
|
+
type,
|
|
1197
|
+
payload,
|
|
1198
|
+
...ids.metadata !== void 0 ? { metadata: ids.metadata } : {}
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// src/agent-protocol/codec.ts
|
|
1203
|
+
var KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
1204
|
+
// node → agent
|
|
1205
|
+
"TASK",
|
|
1206
|
+
"HELLO_ACK",
|
|
1207
|
+
"CONTEXT",
|
|
1208
|
+
"INSTRUCTION",
|
|
1209
|
+
"RESPONSE",
|
|
1210
|
+
"CANCEL",
|
|
1211
|
+
// agent → node
|
|
1212
|
+
"HELLO",
|
|
1213
|
+
"TASK_REQUEST",
|
|
1214
|
+
"TASK_ACCEPTED",
|
|
1215
|
+
"TASK_STARTED",
|
|
1216
|
+
"STATUS",
|
|
1217
|
+
"QUESTION",
|
|
1218
|
+
"REPORT",
|
|
1219
|
+
"ARTIFACT",
|
|
1220
|
+
"COMPLETION",
|
|
1221
|
+
"ERROR",
|
|
1222
|
+
"CANCELLED"
|
|
1223
|
+
]);
|
|
1224
|
+
var PAYLOAD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["TASK", "REPORT", "COMPLETION", "ERROR", "QUESTION", "ARTIFACT", "HELLO"]);
|
|
1225
|
+
function validateEnvelope(value, line) {
|
|
1226
|
+
const sample = line?.slice(0, 200);
|
|
1227
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1228
|
+
return { ok: false, error: { code: "NOT_AN_OBJECT", message: "message must be a JSON object", ...sample ? { sample } : {} } };
|
|
1229
|
+
}
|
|
1230
|
+
const raw = value;
|
|
1231
|
+
const protocol = raw.protocol;
|
|
1232
|
+
if (protocol === void 0) {
|
|
1233
|
+
const type = raw.type;
|
|
1234
|
+
if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
|
|
1235
|
+
return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(type)}`, ...sample ? { sample } : {} } };
|
|
1236
|
+
}
|
|
1237
|
+
return {
|
|
1238
|
+
ok: true,
|
|
1239
|
+
legacy: true,
|
|
1240
|
+
envelope: {
|
|
1241
|
+
protocol: "omninode-agent-protocol/1",
|
|
1242
|
+
messageId: typeof raw.messageId === "string" ? raw.messageId : "legacy",
|
|
1243
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1244
|
+
type,
|
|
1245
|
+
payload: raw.payload
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
if (typeof protocol !== "string" || !isSupportedProtocol(protocol)) {
|
|
1250
|
+
return {
|
|
1251
|
+
ok: false,
|
|
1252
|
+
error: { code: "UNSUPPORTED_VERSION", message: `unsupported protocol "${String(protocol)}"`, ...sample ? { sample } : {} }
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
if (raw.type === void 0) {
|
|
1256
|
+
return { ok: false, error: { code: "MISSING_FIELD", message: "message is missing 'type'", ...sample ? { sample } : {} } };
|
|
1257
|
+
}
|
|
1258
|
+
if (typeof raw.type !== "string" || !KNOWN_TYPES.has(raw.type)) {
|
|
1259
|
+
return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(raw.type)}`, ...sample ? { sample } : {} } };
|
|
1260
|
+
}
|
|
1261
|
+
if (raw.timestamp !== void 0 && typeof raw.timestamp !== "string") {
|
|
1262
|
+
return { ok: false, error: { code: "MISSING_FIELD", message: "'timestamp' must be a string", ...sample ? { sample } : {} } };
|
|
1263
|
+
}
|
|
1264
|
+
if (PAYLOAD_REQUIRED_TYPES.has(raw.type) && (raw.payload === void 0 || raw.payload === null)) {
|
|
1265
|
+
return { ok: false, error: { code: "INVALID_PAYLOAD", message: `${raw.type} requires a payload`, ...sample ? { sample } : {} } };
|
|
1266
|
+
}
|
|
1267
|
+
return {
|
|
1268
|
+
ok: true,
|
|
1269
|
+
legacy: protocol !== PROTOCOL_V2,
|
|
1270
|
+
envelope: {
|
|
1271
|
+
protocol,
|
|
1272
|
+
messageId: typeof raw.messageId === "string" ? raw.messageId : "unknown",
|
|
1273
|
+
...typeof raw.requestId === "string" ? { requestId: raw.requestId } : {},
|
|
1274
|
+
...typeof raw.taskId === "string" ? { taskId: raw.taskId } : {},
|
|
1275
|
+
...typeof raw.agentId === "string" ? { agentId: raw.agentId } : {},
|
|
1276
|
+
...typeof raw.pipelineId === "string" ? { pipelineId: raw.pipelineId } : {},
|
|
1277
|
+
...typeof raw.executionId === "string" ? { executionId: raw.executionId } : {},
|
|
1278
|
+
timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1279
|
+
type: raw.type,
|
|
1280
|
+
payload: raw.payload,
|
|
1281
|
+
...raw.metadata !== null && typeof raw.metadata === "object" ? { metadata: raw.metadata } : {}
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
function decodeMessage(line, maxBytes = MAX_MESSAGE_BYTES) {
|
|
1286
|
+
const trimmed = line.trim();
|
|
1287
|
+
if (trimmed.length === 0) {
|
|
1288
|
+
return { ok: false, error: { code: "INVALID_JSON", message: "empty line" } };
|
|
1289
|
+
}
|
|
1290
|
+
if (trimmed.length > maxBytes) {
|
|
1291
|
+
return {
|
|
1292
|
+
ok: false,
|
|
1293
|
+
error: { code: "OVERSIZED", message: `message of ${trimmed.length} bytes exceeds the ${maxBytes} byte limit`, sample: trimmed.slice(0, 200) }
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
let value;
|
|
1297
|
+
try {
|
|
1298
|
+
value = JSON.parse(trimmed);
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
return {
|
|
1301
|
+
ok: false,
|
|
1302
|
+
error: { code: "INVALID_JSON", message: error instanceof Error ? error.message : "invalid JSON", sample: trimmed.slice(0, 200) }
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
return validateEnvelope(value, trimmed);
|
|
1306
|
+
}
|
|
1307
|
+
function encodeMessage(envelope2, maxBytes = MAX_MESSAGE_BYTES) {
|
|
1308
|
+
const line = JSON.stringify(envelope2);
|
|
1309
|
+
if (line.length > maxBytes) {
|
|
1310
|
+
throw new Error(`outbound protocol message of ${line.length} bytes exceeds the ${maxBytes} byte limit`);
|
|
1311
|
+
}
|
|
1312
|
+
return line;
|
|
1313
|
+
}
|
|
1314
|
+
var ProtocolDecoder = class {
|
|
1315
|
+
buffer = "";
|
|
1316
|
+
maxBytes;
|
|
1317
|
+
constructor(maxBytes = MAX_MESSAGE_BYTES) {
|
|
1318
|
+
this.maxBytes = maxBytes;
|
|
1319
|
+
}
|
|
1320
|
+
push(chunk) {
|
|
1321
|
+
this.buffer += chunk;
|
|
1322
|
+
const results = [];
|
|
1323
|
+
let index = this.buffer.indexOf("\n");
|
|
1324
|
+
while (index >= 0) {
|
|
1325
|
+
const line = this.buffer.slice(0, index);
|
|
1326
|
+
this.buffer = this.buffer.slice(index + 1);
|
|
1327
|
+
if (line.trim().length > 0) results.push(decodeMessage(line, this.maxBytes));
|
|
1328
|
+
index = this.buffer.indexOf("\n");
|
|
1329
|
+
}
|
|
1330
|
+
if (this.buffer.length > this.maxBytes) {
|
|
1331
|
+
results.push({
|
|
1332
|
+
ok: false,
|
|
1333
|
+
error: { code: "OVERSIZED", message: `line exceeded ${this.maxBytes} bytes without a newline`, sample: this.buffer.slice(0, 200) }
|
|
1334
|
+
});
|
|
1335
|
+
this.buffer = "";
|
|
1336
|
+
}
|
|
1337
|
+
return results;
|
|
1338
|
+
}
|
|
1339
|
+
/** Flush any trailing line when the process exits. */
|
|
1340
|
+
flush() {
|
|
1341
|
+
if (this.buffer.trim().length === 0) {
|
|
1342
|
+
this.buffer = "";
|
|
1343
|
+
return [];
|
|
1344
|
+
}
|
|
1345
|
+
const result = decodeMessage(this.buffer, this.maxBytes);
|
|
1346
|
+
this.buffer = "";
|
|
1347
|
+
return [result];
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
// src/agent-protocol/handshake.ts
|
|
1352
|
+
function parseHello(envelopeValue) {
|
|
1353
|
+
if (envelopeValue.type !== "HELLO") {
|
|
1354
|
+
return { ok: false, reason: `expected HELLO, received ${envelopeValue.type}` };
|
|
1355
|
+
}
|
|
1356
|
+
const payload = envelopeValue.payload;
|
|
1357
|
+
if (payload === null || typeof payload !== "object") {
|
|
1358
|
+
return { ok: false, reason: "HELLO payload must be an object" };
|
|
1359
|
+
}
|
|
1360
|
+
const p = payload;
|
|
1361
|
+
if (typeof p.name !== "string" || p.name.length === 0) {
|
|
1362
|
+
return { ok: false, reason: "HELLO payload is missing 'name'" };
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
ok: true,
|
|
1366
|
+
descriptor: {
|
|
1367
|
+
protocol: envelopeValue.protocol,
|
|
1368
|
+
name: p.name,
|
|
1369
|
+
...typeof p.version === "string" ? { version: p.version } : {},
|
|
1370
|
+
capabilities: Array.isArray(p.capabilities) ? p.capabilities.filter((c) => typeof c === "string") : [],
|
|
1371
|
+
supports: Array.isArray(p.supports) ? p.supports.filter((s) => typeof s === "string") : []
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
function buildHelloAck(descriptor) {
|
|
1376
|
+
return envelope("HELLO_ACK", {
|
|
1377
|
+
ok: true,
|
|
1378
|
+
protocol: PROTOCOL_V2,
|
|
1379
|
+
agent: descriptor.name,
|
|
1380
|
+
...descriptor.version !== void 0 ? { agentVersion: descriptor.version } : {},
|
|
1381
|
+
supportedProtocols: [PROTOCOL_V2]
|
|
1382
|
+
}, { agentId: descriptor.name });
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// src/agent-protocol/session.ts
|
|
1386
|
+
var ProtocolSession = class {
|
|
1387
|
+
decoder;
|
|
1388
|
+
ids;
|
|
1389
|
+
responder;
|
|
1390
|
+
violations = [];
|
|
1391
|
+
questions = [];
|
|
1392
|
+
artifacts = [];
|
|
1393
|
+
reports = [];
|
|
1394
|
+
descriptor;
|
|
1395
|
+
completionSummary;
|
|
1396
|
+
errorMessage;
|
|
1397
|
+
constructor(ids, options = {}) {
|
|
1398
|
+
this.ids = ids;
|
|
1399
|
+
this.responder = options.responder;
|
|
1400
|
+
this.decoder = new ProtocolDecoder(options.maxBytes);
|
|
1401
|
+
}
|
|
1402
|
+
/** The v2 messages OmniNode sends to open a task (written to the agent's stdin). */
|
|
1403
|
+
taskMessages(objective, role, context, instruction) {
|
|
1404
|
+
const ids = this.ids;
|
|
1405
|
+
const messages = [envelope("TASK", { taskId: ids.taskId, objective }, ids)];
|
|
1406
|
+
if (role !== void 0) messages.push(envelope("CONTEXT", { role }, ids));
|
|
1407
|
+
if (context !== void 0 && context.length > 0) messages.push(envelope("CONTEXT", { body: context }, ids));
|
|
1408
|
+
if (instruction !== void 0 && instruction.length > 0) messages.push(envelope("INSTRUCTION", { text: instruction }, ids));
|
|
1409
|
+
return messages.map((m) => encodeMessage(m));
|
|
1410
|
+
}
|
|
1411
|
+
helloAck(descriptor) {
|
|
1412
|
+
return encodeMessage(buildHelloAck(descriptor));
|
|
1413
|
+
}
|
|
1414
|
+
/** Consumes decoded stdout, answering questions as they arrive. */
|
|
1415
|
+
async handle(decoded) {
|
|
1416
|
+
if (!decoded.ok) {
|
|
1417
|
+
this.violations.push(decoded.error);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const message = decoded.envelope;
|
|
1421
|
+
if (message.protocol === PROTOCOL_V2) this.sawV2 = true;
|
|
1422
|
+
if (message.type === "HELLO") {
|
|
1423
|
+
const hello = parseHello(message);
|
|
1424
|
+
if (hello.ok) this.descriptor = hello.descriptor;
|
|
1425
|
+
else this.violations.push({ code: "INVALID_PAYLOAD", message: `bad HELLO: ${hello.reason}` });
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (message.type === "QUESTION") {
|
|
1429
|
+
await this.answerQuestion(message);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
if (message.type === "REPORT") {
|
|
1433
|
+
this.reports.push(message.payload);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (message.type === "ARTIFACT") {
|
|
1437
|
+
this.artifacts.push(message.payload);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
if (message.type === "COMPLETION") {
|
|
1441
|
+
const payload = message.payload;
|
|
1442
|
+
this.completionSummary = typeof payload?.summary === "string" ? payload.summary : "";
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
if (message.type === "ERROR") {
|
|
1446
|
+
const payload = message.payload;
|
|
1447
|
+
const detail = typeof payload?.message === "string" ? payload.message : "agent reported an error";
|
|
1448
|
+
const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
|
|
1449
|
+
this.errorMessage = `Agent error${code}: ${detail}`;
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
if (message.type === "TASK_ACCEPTED" || message.type === "TASK_STARTED" || message.type === "STATUS") {
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
this.violations.push({
|
|
1456
|
+
code: "UNKNOWN_TYPE",
|
|
1457
|
+
message: `unexpected message type ${message.type} from agent`
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
async answerQuestion(message) {
|
|
1461
|
+
const payload = message.payload ?? {};
|
|
1462
|
+
const questionId = typeof payload.questionId === "string" ? payload.questionId : message.messageId;
|
|
1463
|
+
const question = typeof payload.question === "string" ? payload.question : "";
|
|
1464
|
+
const expected = typeof payload.expected === "string" ? payload.expected : "text";
|
|
1465
|
+
let answer;
|
|
1466
|
+
if (this.responder) {
|
|
1467
|
+
try {
|
|
1468
|
+
answer = await this.responder({ questionId, question, expected });
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
this.violations.push({
|
|
1471
|
+
code: "INVALID_PAYLOAD",
|
|
1472
|
+
message: `question responder failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
this.questions.push({ questionId, question, ...answer !== void 0 ? { answer } : {} });
|
|
1477
|
+
this.pendingAnswers.push(
|
|
1478
|
+
encodeMessage(
|
|
1479
|
+
envelope("RESPONSE", { questionId, ...answer !== void 0 ? { body: answer } : {} }, {
|
|
1480
|
+
...this.ids,
|
|
1481
|
+
requestId: message.requestId ?? this.ids.requestId
|
|
1482
|
+
})
|
|
1483
|
+
)
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
pendingAnswers = [];
|
|
1487
|
+
/** True once any inbound message carried the v2 protocol field. */
|
|
1488
|
+
sawV2 = false;
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
// src/agents/cwd.ts
|
|
1492
|
+
import { statSync } from "fs";
|
|
1493
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
1494
|
+
function validateWorkingDirectory(cwd, options = {}) {
|
|
1495
|
+
if (!cwd) return void 0;
|
|
1496
|
+
const normalized = isAbsolute(cwd) ? resolve(cwd) : resolve(process.cwd(), cwd);
|
|
1497
|
+
let stats;
|
|
1498
|
+
try {
|
|
1499
|
+
stats = statSync(normalized);
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
throw new AgentError(
|
|
1502
|
+
"AGENT_NOT_FOUND",
|
|
1503
|
+
`Working directory "${normalized}" does not exist or is not accessible.`,
|
|
1504
|
+
{ cause: error, details: { cwd: normalized } }
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
if (!stats.isDirectory()) {
|
|
1508
|
+
throw new AgentError("AGENT_NOT_FOUND", `Working directory "${normalized}" is not a directory.`, {
|
|
1509
|
+
details: { cwd: normalized }
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
try {
|
|
1513
|
+
statSync(resolve(normalized, "."));
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
throw new AgentError(
|
|
1516
|
+
"AGENT_NOT_FOUND",
|
|
1517
|
+
`Working directory "${normalized}" is not accessible.`,
|
|
1518
|
+
{ cause: error }
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
1522
|
+
const rel = relative(projectRoot, normalized);
|
|
1523
|
+
const outsideProject = rel.startsWith("..") || isAbsolute(rel);
|
|
1524
|
+
if (outsideProject && options.allowExternal !== true) {
|
|
1525
|
+
throw new AgentError(
|
|
1526
|
+
"AGENT_NOT_FOUND",
|
|
1527
|
+
`Working directory "${normalized}" is outside the project root "${projectRoot}". Set allow_external_cwd: true on the agent to permit this.`,
|
|
1528
|
+
{ details: { cwd: normalized, projectRoot } }
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
return { cwd: normalized, outsideProject };
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// src/agents/env.ts
|
|
1535
|
+
var BASELINE_KEYS = ["PATH", "HOME"];
|
|
1536
|
+
function resolveEnvPolicy(config) {
|
|
1537
|
+
if (config.envPolicy !== void 0) return config.envPolicy;
|
|
1538
|
+
if (config.inheritEnv === false) return "explicit";
|
|
1539
|
+
return "inherit";
|
|
1540
|
+
}
|
|
902
1541
|
function buildChildEnv(config) {
|
|
903
1542
|
const configured = config.env ?? {};
|
|
904
|
-
|
|
1543
|
+
const policy = resolveEnvPolicy(config);
|
|
1544
|
+
if (policy === "inherit") {
|
|
905
1545
|
return { ...process.env, ...configured };
|
|
906
1546
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
}
|
|
1547
|
+
const base = {};
|
|
1548
|
+
for (const key of BASELINE_KEYS) {
|
|
1549
|
+
const value = process.env[key];
|
|
1550
|
+
if (value !== void 0) base[key] = value;
|
|
1551
|
+
}
|
|
1552
|
+
switch (policy) {
|
|
1553
|
+
case "explicit":
|
|
1554
|
+
return { ...base, ...configured };
|
|
1555
|
+
case "allowlist": {
|
|
1556
|
+
const allowed = { ...base };
|
|
1557
|
+
for (const key of config.envAllowlist ?? []) {
|
|
1558
|
+
const value = process.env[key];
|
|
1559
|
+
if (value !== void 0) allowed[key] = value;
|
|
1560
|
+
}
|
|
1561
|
+
return { ...allowed, ...configured };
|
|
1562
|
+
}
|
|
1563
|
+
case "denylist": {
|
|
1564
|
+
const denied = new Set((config.envDenylist ?? []).map((k) => k.toUpperCase()));
|
|
1565
|
+
const filtered = {};
|
|
1566
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
1567
|
+
if (denied.has(key.toUpperCase())) continue;
|
|
1568
|
+
filtered[key] = value;
|
|
1569
|
+
}
|
|
1570
|
+
return { ...filtered, ...configured };
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
912
1573
|
}
|
|
1574
|
+
|
|
1575
|
+
// src/agents/process/index.ts
|
|
1576
|
+
var DEFAULT_TIMEOUT_MS2 = 6e5;
|
|
1577
|
+
var KILL_GRACE_MS = 5e3;
|
|
1578
|
+
var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
913
1579
|
function composeTaskText(input) {
|
|
914
1580
|
const sections = [`# Objective
|
|
915
1581
|
${input.objective}`];
|
|
@@ -930,18 +1596,19 @@ ${input.instruction}`);
|
|
|
930
1596
|
return sections.join("\n\n");
|
|
931
1597
|
}
|
|
932
1598
|
function buildProtocolMessages(input, agentName) {
|
|
933
|
-
const
|
|
1599
|
+
const session = new ProtocolSession({
|
|
1600
|
+
requestId: `req-${input.taskId}`,
|
|
934
1601
|
taskId: input.taskId,
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1602
|
+
agentId: agentName,
|
|
1603
|
+
...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
|
|
1604
|
+
...input.executionId !== void 0 ? { executionId: input.executionId } : {}
|
|
1605
|
+
});
|
|
1606
|
+
return session.taskMessages(
|
|
1607
|
+
input.objective,
|
|
1608
|
+
input.role,
|
|
1609
|
+
input.context,
|
|
1610
|
+
input.instruction
|
|
1611
|
+
);
|
|
945
1612
|
}
|
|
946
1613
|
var ProcessAgent = class {
|
|
947
1614
|
config;
|
|
@@ -970,40 +1637,73 @@ var ProcessAgent = class {
|
|
|
970
1637
|
}
|
|
971
1638
|
const mode = this.config.inputMode ?? "stdin";
|
|
972
1639
|
const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
973
|
-
return new Promise((
|
|
1640
|
+
return new Promise((resolve2, reject) => {
|
|
1641
|
+
let cwd;
|
|
1642
|
+
try {
|
|
1643
|
+
cwd = validateWorkingDirectory(this.config.cwd, {
|
|
1644
|
+
allowExternal: this.config.allowExternalCwd
|
|
1645
|
+
})?.cwd;
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
reject(error);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
974
1650
|
const child = spawn(
|
|
975
1651
|
command,
|
|
976
1652
|
[...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
|
|
977
1653
|
{
|
|
978
|
-
cwd:
|
|
1654
|
+
...cwd !== void 0 ? { cwd } : {},
|
|
979
1655
|
env: buildChildEnv(this.config),
|
|
980
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1656
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1657
|
+
// Own process group so the whole tree can be terminated together
|
|
1658
|
+
// (roadmap §8: no orphaned children).
|
|
1659
|
+
...process.platform === "win32" ? {} : { detached: true }
|
|
981
1660
|
}
|
|
982
1661
|
);
|
|
983
1662
|
this.running.set(input.taskId, child);
|
|
1663
|
+
GLOBAL_RUNNING.add(child);
|
|
1664
|
+
installSignalForwarding();
|
|
984
1665
|
let stdout = "";
|
|
985
1666
|
let stderr = "";
|
|
986
1667
|
let timedOut = false;
|
|
1668
|
+
let outputLimitExceeded = false;
|
|
987
1669
|
let settled = false;
|
|
1670
|
+
const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
988
1671
|
const timer = setTimeout(() => {
|
|
989
1672
|
timedOut = true;
|
|
990
|
-
this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
|
|
991
|
-
child
|
|
992
|
-
setTimeout(() => child
|
|
1673
|
+
this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process tree.`);
|
|
1674
|
+
killTree(child, "SIGTERM");
|
|
1675
|
+
setTimeout(() => killTree(child, "SIGKILL"), KILL_GRACE_MS).unref();
|
|
993
1676
|
}, timeoutMs);
|
|
994
1677
|
const settle = (finish) => {
|
|
995
1678
|
if (settled) return;
|
|
996
1679
|
settled = true;
|
|
997
1680
|
clearTimeout(timer);
|
|
998
1681
|
this.running.delete(input.taskId);
|
|
1682
|
+
GLOBAL_RUNNING.delete(child);
|
|
999
1683
|
finish();
|
|
1000
1684
|
};
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1685
|
+
const collect = (into, chunk) => {
|
|
1686
|
+
if (outputLimitExceeded) return;
|
|
1687
|
+
const text = sanitizeChunk(chunk);
|
|
1688
|
+
if (into === "stdout") {
|
|
1689
|
+
if (stdout.length + text.length > maxOutputBytes) {
|
|
1690
|
+
stdout = (stdout + text).slice(0, maxOutputBytes);
|
|
1691
|
+
outputLimitExceeded = true;
|
|
1692
|
+
this.log.warn(`Agent "${this.config.name}" exceeded the ${maxOutputBytes} byte output limit \u2014 killing it.`);
|
|
1693
|
+
killTree(child, "SIGKILL");
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
stdout += text;
|
|
1697
|
+
} else {
|
|
1698
|
+
if (stderr.length + text.length > maxOutputBytes) {
|
|
1699
|
+
stderr = (stderr + text).slice(0, maxOutputBytes);
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
stderr += text;
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
child.stdout?.on("data", (chunk) => collect("stdout", chunk));
|
|
1706
|
+
child.stderr?.on("data", (chunk) => collect("stderr", chunk));
|
|
1007
1707
|
child.stdin?.on("error", () => {
|
|
1008
1708
|
});
|
|
1009
1709
|
if (mode !== "arg") {
|
|
@@ -1037,7 +1737,19 @@ var ProcessAgent = class {
|
|
|
1037
1737
|
new AgentError(
|
|
1038
1738
|
"AGENT_TIMEOUT",
|
|
1039
1739
|
`Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
|
|
1040
|
-
{
|
|
1740
|
+
{
|
|
1741
|
+
details: {
|
|
1742
|
+
operation: "agent.run",
|
|
1743
|
+
agent: this.config.name,
|
|
1744
|
+
durationMs: timeoutMs,
|
|
1745
|
+
// The process was started and may have performed side
|
|
1746
|
+
// effects before being killed — the outcome is unknown,
|
|
1747
|
+
// not failed (roadmap §6.3).
|
|
1748
|
+
dispatched: true,
|
|
1749
|
+
stdout: stdout.slice(0, 4e3),
|
|
1750
|
+
stderr: stderr.slice(0, 2e3)
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1041
1753
|
)
|
|
1042
1754
|
);
|
|
1043
1755
|
return;
|
|
@@ -1045,7 +1757,7 @@ var ProcessAgent = class {
|
|
|
1045
1757
|
const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
|
|
1046
1758
|
if (mode === "protocol") {
|
|
1047
1759
|
try {
|
|
1048
|
-
|
|
1760
|
+
resolve2(
|
|
1049
1761
|
interpretProtocolRun({
|
|
1050
1762
|
input,
|
|
1051
1763
|
agentName: this.config.name,
|
|
@@ -1060,14 +1772,14 @@ var ProcessAgent = class {
|
|
|
1060
1772
|
return;
|
|
1061
1773
|
}
|
|
1062
1774
|
const trimmed = stdout.trim();
|
|
1063
|
-
|
|
1775
|
+
resolve2({
|
|
1064
1776
|
taskId: input.taskId,
|
|
1065
|
-
status: code === 0 ? "completed" : "failed",
|
|
1777
|
+
status: code === 0 && !outputLimitExceeded ? "completed" : "failed",
|
|
1066
1778
|
summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
|
|
1067
1779
|
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1068
1780
|
exitCode: code ?? void 0,
|
|
1069
1781
|
logs,
|
|
1070
|
-
error:
|
|
1782
|
+
...outputLimitExceeded ? { error: `Agent exceeded the ${maxOutputBytes} byte output limit and was terminated.` } : code === 0 ? {} : { error: describeExit(code, signal) }
|
|
1071
1783
|
});
|
|
1072
1784
|
});
|
|
1073
1785
|
});
|
|
@@ -1076,67 +1788,117 @@ var ProcessAgent = class {
|
|
|
1076
1788
|
/** Ask the agent to stop current work (SIGTERM). */
|
|
1077
1789
|
async cancel(taskId) {
|
|
1078
1790
|
const child = this.running.get(taskId);
|
|
1079
|
-
if (child) child
|
|
1791
|
+
if (child) killTree(child, "SIGTERM");
|
|
1080
1792
|
}
|
|
1081
1793
|
runningTaskIds() {
|
|
1082
1794
|
return [...this.running.keys()];
|
|
1083
1795
|
}
|
|
1084
1796
|
};
|
|
1085
|
-
function interpretProtocolRun({
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
|
|
1106
|
-
error = `Agent error${code}: ${detail}`;
|
|
1107
|
-
}
|
|
1797
|
+
function interpretProtocolRun({
|
|
1798
|
+
input,
|
|
1799
|
+
agentName,
|
|
1800
|
+
stdout,
|
|
1801
|
+
exitCode,
|
|
1802
|
+
logs,
|
|
1803
|
+
responder
|
|
1804
|
+
}) {
|
|
1805
|
+
const session = new ProtocolSession(
|
|
1806
|
+
{
|
|
1807
|
+
requestId: `req-${input.taskId}`,
|
|
1808
|
+
taskId: input.taskId,
|
|
1809
|
+
agentId: agentName,
|
|
1810
|
+
...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
|
|
1811
|
+
...input.executionId !== void 0 ? { executionId: input.executionId } : {}
|
|
1812
|
+
},
|
|
1813
|
+
{ ...responder !== void 0 ? { responder } : {} }
|
|
1814
|
+
);
|
|
1815
|
+
for (const result of session.decoder.push(stdout)) {
|
|
1816
|
+
session.handle(result);
|
|
1108
1817
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
`Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
|
|
1112
|
-
{ details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
|
|
1113
|
-
);
|
|
1818
|
+
for (const result of session.decoder.flush()) {
|
|
1819
|
+
session.handle(result);
|
|
1114
1820
|
}
|
|
1821
|
+
const reports = session.reports.flatMap((payload) => Array.isArray(payload) ? payload : [payload]).map((payload, index) => normalizeReport(payload, input.taskId, agentName, index)).filter((report) => report !== void 0);
|
|
1115
1822
|
const trimmed = stdout.trim();
|
|
1823
|
+
const protocolBlock = {
|
|
1824
|
+
protocol: session.descriptor?.protocol ?? PROTOCOL_V2,
|
|
1825
|
+
legacy: !session.sawV2 && session.descriptor === void 0,
|
|
1826
|
+
...session.descriptor !== void 0 ? { descriptor: session.descriptor } : {},
|
|
1827
|
+
questions: session.questions,
|
|
1828
|
+
violations: session.violations.map((v) => `${v.code}: ${v.message}`)
|
|
1829
|
+
};
|
|
1830
|
+
if (session.errorMessage !== void 0) {
|
|
1831
|
+
return {
|
|
1832
|
+
taskId: input.taskId,
|
|
1833
|
+
status: "failed",
|
|
1834
|
+
error: session.errorMessage,
|
|
1835
|
+
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1836
|
+
exitCode: exitCode ?? void 0,
|
|
1837
|
+
logs,
|
|
1838
|
+
protocol: protocolBlock,
|
|
1839
|
+
...reports.length > 0 ? { reports } : {}
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
if (session.descriptor === void 0 && session.violations.length > 0 && session.reports.length === 0 && session.completionSummary === void 0) {
|
|
1843
|
+
return {
|
|
1844
|
+
taskId: input.taskId,
|
|
1845
|
+
status: "failed",
|
|
1846
|
+
error: `agent produced no valid protocol messages: ${protocolBlock.violations.join("; ")}`,
|
|
1847
|
+
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1848
|
+
exitCode: exitCode ?? void 0,
|
|
1849
|
+
logs,
|
|
1850
|
+
protocol: protocolBlock
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
const completed = exitCode === 0 && session.completionSummary !== void 0;
|
|
1116
1854
|
return {
|
|
1117
1855
|
taskId: input.taskId,
|
|
1118
|
-
status:
|
|
1119
|
-
summary:
|
|
1856
|
+
status: completed || exitCode === 0 ? completed ? "completed" : "completed" : "failed",
|
|
1857
|
+
summary: session.completionSummary ?? reports[0]?.summary,
|
|
1120
1858
|
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1121
1859
|
exitCode: exitCode ?? void 0,
|
|
1122
1860
|
logs,
|
|
1123
|
-
|
|
1124
|
-
|
|
1861
|
+
protocol: protocolBlock,
|
|
1862
|
+
...reports.length > 0 ? { reports } : {},
|
|
1863
|
+
...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
|
|
1125
1864
|
};
|
|
1126
1865
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1866
|
+
var GLOBAL_RUNNING = /* @__PURE__ */ new Set();
|
|
1867
|
+
var signalForwardingInstalled = false;
|
|
1868
|
+
function installSignalForwarding() {
|
|
1869
|
+
if (signalForwardingInstalled) return;
|
|
1870
|
+
signalForwardingInstalled = true;
|
|
1871
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
1872
|
+
process.on(signal, () => {
|
|
1873
|
+
for (const child of GLOBAL_RUNNING) {
|
|
1874
|
+
killTree(child, "SIGTERM");
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
function killTree(child, signal) {
|
|
1880
|
+
if (child.pid === void 0) return;
|
|
1881
|
+
if (process.platform !== "win32") {
|
|
1882
|
+
try {
|
|
1883
|
+
process.kill(-child.pid, signal);
|
|
1884
|
+
return;
|
|
1885
|
+
} catch {
|
|
1136
1886
|
}
|
|
1887
|
+
}
|
|
1888
|
+
try {
|
|
1889
|
+
child.kill(signal);
|
|
1137
1890
|
} catch {
|
|
1138
1891
|
}
|
|
1139
|
-
|
|
1892
|
+
}
|
|
1893
|
+
function describeExit(code, signal) {
|
|
1894
|
+
if (signal !== null) {
|
|
1895
|
+
return signal === "SIGKILL" ? "Process was killed (SIGKILL) \u2014 usually an external kill or OOM." : `Process terminated by signal ${signal}.`;
|
|
1896
|
+
}
|
|
1897
|
+
if (code === null) return "Process terminated without an exit code.";
|
|
1898
|
+
return `Process exited with code ${code}.`;
|
|
1899
|
+
}
|
|
1900
|
+
function sanitizeChunk(chunk) {
|
|
1901
|
+
return [...chunk.toString("utf8")].filter((char) => char.charCodeAt(0) !== 0).join("");
|
|
1140
1902
|
}
|
|
1141
1903
|
function normalizeReport(payload, taskId, agentName, index) {
|
|
1142
1904
|
if (payload === null || typeof payload !== "object") return void 0;
|
|
@@ -1157,6 +1919,40 @@ function firstLine(text, maxLength = 200) {
|
|
|
1157
1919
|
return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
|
|
1158
1920
|
}
|
|
1159
1921
|
|
|
1922
|
+
// src/agents/adapters.ts
|
|
1923
|
+
var AgentAdapterRegistry = class {
|
|
1924
|
+
adapters = /* @__PURE__ */ new Map();
|
|
1925
|
+
register(adapter) {
|
|
1926
|
+
this.adapters.set(adapter.integration, adapter);
|
|
1927
|
+
}
|
|
1928
|
+
get(integration) {
|
|
1929
|
+
return this.adapters.get(integration);
|
|
1930
|
+
}
|
|
1931
|
+
list() {
|
|
1932
|
+
return [...this.adapters.values()];
|
|
1933
|
+
}
|
|
1934
|
+
create(config) {
|
|
1935
|
+
const adapter = this.get(config.integration);
|
|
1936
|
+
if (!adapter) {
|
|
1937
|
+
throw new AgentError(
|
|
1938
|
+
"AGENT_NOT_FOUND",
|
|
1939
|
+
`No agent adapter registered for integration "${config.integration}". Registered: ${[...this.adapters.keys()].join(", ") || "(none)"}.`
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
return adapter.create(config);
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
var ProcessAgentAdapter = class {
|
|
1946
|
+
constructor(factory) {
|
|
1947
|
+
this.factory = factory;
|
|
1948
|
+
}
|
|
1949
|
+
factory;
|
|
1950
|
+
integration = "process";
|
|
1951
|
+
create(config) {
|
|
1952
|
+
return this.factory(config);
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
|
|
1160
1956
|
// src/agents/registry.ts
|
|
1161
1957
|
var AgentRegistry = class {
|
|
1162
1958
|
agents = /* @__PURE__ */ new Map();
|
|
@@ -1172,14 +1968,15 @@ var AgentRegistry = class {
|
|
|
1172
1968
|
};
|
|
1173
1969
|
|
|
1174
1970
|
// src/agents/factory.ts
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1971
|
+
var defaultAgentAdapters = new AgentAdapterRegistry();
|
|
1972
|
+
defaultAgentAdapters.register(new ProcessAgentAdapter((config) => new ProcessAgent(config)));
|
|
1973
|
+
function createAgent(config, adapters = defaultAgentAdapters) {
|
|
1974
|
+
const registered = adapters.get(config.integration);
|
|
1975
|
+
if (registered) return registered.create(config);
|
|
1179
1976
|
if (config.integration === "native") {
|
|
1180
1977
|
throw new OmniNodeError(
|
|
1181
1978
|
"NOT_IMPLEMENTED",
|
|
1182
|
-
`
|
|
1979
|
+
`No native adapter is registered for "${config.name}". Register an IAgentAdapter (createAgent(config, adapters)) or use input over stdin/stdout (type "cli").`
|
|
1183
1980
|
);
|
|
1184
1981
|
}
|
|
1185
1982
|
throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
|
|
@@ -1222,7 +2019,7 @@ function registerAgentCommands(program) {
|
|
|
1222
2019
|
"--input-mode <mode>",
|
|
1223
2020
|
"How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
|
|
1224
2021
|
"stdin"
|
|
1225
|
-
).option(
|
|
2022
|
+
).option("--env-policy <policy>", "Environment policy: inherit, allowlist, denylist or explicit.").option("--allow-env <name>", "Variable to forward under the allowlist policy (repeatable).", collectArgs).option("--deny-env <name>", "Variable to remove under the denylist policy (repeatable).", collectArgs).option("--max-output-bytes <n>", "Per-stream output budget in bytes (default 5 MiB).", (value) => Number(value)).option("--allow-external-cwd", "Allow a working directory outside the project root.").option(
|
|
1226
2023
|
"--timeout-ms <ms>",
|
|
1227
2024
|
"Per-task timeout in milliseconds.",
|
|
1228
2025
|
(value) => Number(value)
|
|
@@ -1237,7 +2034,12 @@ async function addAgent(options) {
|
|
|
1237
2034
|
...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
|
|
1238
2035
|
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
1239
2036
|
input_mode: options.inputMode,
|
|
1240
|
-
...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {}
|
|
2037
|
+
...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
|
|
2038
|
+
...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
|
|
2039
|
+
...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
|
|
2040
|
+
...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
|
|
2041
|
+
...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
|
|
2042
|
+
...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
|
|
1241
2043
|
});
|
|
1242
2044
|
if (!candidate.success) {
|
|
1243
2045
|
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
@@ -1302,40 +2104,54 @@ function registerRoleCommands(program) {
|
|
|
1302
2104
|
});
|
|
1303
2105
|
}
|
|
1304
2106
|
|
|
1305
|
-
// src/
|
|
2107
|
+
// src/persistence/json-file-store.ts
|
|
1306
2108
|
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
1307
2109
|
import { dirname } from "path";
|
|
1308
|
-
var
|
|
2110
|
+
var JsonFileStore = class {
|
|
1309
2111
|
filePath;
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
*/
|
|
2112
|
+
schemaVersion;
|
|
2113
|
+
onCorrupt;
|
|
2114
|
+
log;
|
|
1314
2115
|
queue = Promise.resolve();
|
|
1315
|
-
constructor(
|
|
1316
|
-
this.filePath = `${directory}
|
|
2116
|
+
constructor(options) {
|
|
2117
|
+
this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
|
|
2118
|
+
this.schemaVersion = options.schemaVersion ?? 1;
|
|
2119
|
+
this.onCorrupt = options.onCorrupt;
|
|
2120
|
+
this.log = logger.child({ component: "persistence", file: options.fileName });
|
|
1317
2121
|
}
|
|
1318
|
-
|
|
1319
|
-
return this.
|
|
1320
|
-
const tasks = await this.load();
|
|
1321
|
-
const index = tasks.findIndex((t) => t.id === task.id);
|
|
1322
|
-
if (index >= 0) tasks[index] = task;
|
|
1323
|
-
else tasks.push(task);
|
|
1324
|
-
await this.write(tasks);
|
|
1325
|
-
});
|
|
2122
|
+
get path() {
|
|
2123
|
+
return this.filePath;
|
|
1326
2124
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
2125
|
+
/** All items, or [] when missing/corrupt. */
|
|
2126
|
+
async readAll() {
|
|
2127
|
+
let raw;
|
|
2128
|
+
try {
|
|
2129
|
+
raw = await readFile(this.filePath, "utf8");
|
|
2130
|
+
} catch {
|
|
2131
|
+
return [];
|
|
2132
|
+
}
|
|
2133
|
+
try {
|
|
2134
|
+
const parsed = JSON.parse(raw);
|
|
2135
|
+
if (Array.isArray(parsed)) return parsed;
|
|
2136
|
+
if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.items)) {
|
|
2137
|
+
return parsed.items;
|
|
2138
|
+
}
|
|
2139
|
+
throw new Error("persisted file has an unrecognized shape");
|
|
2140
|
+
} catch (error) {
|
|
2141
|
+
this.log.warn(`Treating ${this.filePath} as empty \u2014 it could not be parsed.`);
|
|
2142
|
+
this.onCorrupt?.(this.filePath, error);
|
|
2143
|
+
return [];
|
|
2144
|
+
}
|
|
1332
2145
|
}
|
|
1333
|
-
async
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
}
|
|
2146
|
+
async writeAll(items) {
|
|
2147
|
+
const document = { schemaVersion: this.schemaVersion, items };
|
|
2148
|
+
const tmp = `${this.filePath}.tmp`;
|
|
2149
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
2150
|
+
await writeFile(tmp, `${JSON.stringify(document, null, 2)}
|
|
2151
|
+
`, "utf8");
|
|
2152
|
+
await rename(tmp, this.filePath);
|
|
1338
2153
|
}
|
|
2154
|
+
/** Serializes an async operation against this file. */
|
|
1339
2155
|
synchronized(operation) {
|
|
1340
2156
|
const result = this.queue.then(operation, operation);
|
|
1341
2157
|
this.queue = result.then(
|
|
@@ -1344,21 +2160,37 @@ var FileTaskStore = class {
|
|
|
1344
2160
|
);
|
|
1345
2161
|
return result;
|
|
1346
2162
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
return
|
|
1354
|
-
}
|
|
2163
|
+
/** Read-modify-write helper: applies `mutate` to the current items. */
|
|
2164
|
+
async mutate(mutate) {
|
|
2165
|
+
return this.synchronized(async () => {
|
|
2166
|
+
const items = await this.readAll();
|
|
2167
|
+
const [next, result] = await mutate(items);
|
|
2168
|
+
await this.writeAll(next);
|
|
2169
|
+
return result;
|
|
2170
|
+
});
|
|
1355
2171
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
2172
|
+
};
|
|
2173
|
+
|
|
2174
|
+
// src/tasks/store.ts
|
|
2175
|
+
var FileTaskStore = class extends JsonFileStore {
|
|
2176
|
+
constructor(directory) {
|
|
2177
|
+
super({ ...directory !== void 0 ? { directory } : {}, fileName: "tasks.json" });
|
|
2178
|
+
}
|
|
2179
|
+
save(task) {
|
|
2180
|
+
return this.mutate((tasks) => {
|
|
2181
|
+
const index = tasks.findIndex((t) => t.id === task.id);
|
|
2182
|
+
if (index >= 0) tasks[index] = task;
|
|
2183
|
+
else tasks.push(task);
|
|
2184
|
+
return [tasks, void 0];
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
async get(id) {
|
|
2188
|
+
const tasks = await this.readAll();
|
|
2189
|
+
return tasks.find((t) => t.id === id);
|
|
2190
|
+
}
|
|
2191
|
+
async list(filter = {}) {
|
|
2192
|
+
const tasks = await this.readAll();
|
|
2193
|
+
return tasks.filter((task) => filter.status ? task.status === filter.status : true).filter((task) => filter.project ? task.project === filter.project : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1362
2194
|
}
|
|
1363
2195
|
};
|
|
1364
2196
|
|
|
@@ -1367,12 +2199,18 @@ import { randomBytes } from "crypto";
|
|
|
1367
2199
|
var ALLOWED_TRANSITIONS = {
|
|
1368
2200
|
created: ["queued", "running", "cancelled"],
|
|
1369
2201
|
queued: ["running", "cancelled"],
|
|
1370
|
-
running: ["completed", "failed", "cancelled"],
|
|
2202
|
+
running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
|
|
1371
2203
|
waiting: ["running", "cancelled"],
|
|
1372
2204
|
completed: [],
|
|
1373
|
-
failed:
|
|
1374
|
-
|
|
2205
|
+
// failed → running is the only re-entry: bounded automatic retry (§6.4).
|
|
2206
|
+
failed: ["running"],
|
|
2207
|
+
timed_out: [],
|
|
2208
|
+
cancelled: [],
|
|
2209
|
+
unknown: [],
|
|
2210
|
+
partially_completed: []
|
|
1375
2211
|
};
|
|
2212
|
+
var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
|
|
2213
|
+
var EXECUTION_HISTORY_LIMIT = 10;
|
|
1376
2214
|
var TaskEngine = class {
|
|
1377
2215
|
constructor(options) {
|
|
1378
2216
|
this.options = options;
|
|
@@ -1404,6 +2242,7 @@ var TaskEngine = class {
|
|
|
1404
2242
|
id: generateTaskId(),
|
|
1405
2243
|
objective: input.objective.trim(),
|
|
1406
2244
|
status: "created",
|
|
2245
|
+
attempt: 0,
|
|
1407
2246
|
createdAt: now,
|
|
1408
2247
|
updatedAt: now,
|
|
1409
2248
|
...input.project !== void 0 ? { project: input.project } : {},
|
|
@@ -1420,8 +2259,10 @@ var TaskEngine = class {
|
|
|
1420
2259
|
const task = await this.mustGet(id);
|
|
1421
2260
|
return this.transition(task, "queued");
|
|
1422
2261
|
}
|
|
1423
|
-
/** Runs the task through its assigned agent and records the result.
|
|
1424
|
-
|
|
2262
|
+
/** Runs the task through its assigned agent and records the result.
|
|
2263
|
+
* `maxAttempts` enables bounded automatic retries of *retryable* errors
|
|
2264
|
+
* with linear backoff; unknown/timed_out outcomes are never auto-retried. */
|
|
2265
|
+
async run(id, options = {}) {
|
|
1425
2266
|
const task = await this.mustGet(id);
|
|
1426
2267
|
if (task.status !== "created" && task.status !== "queued") {
|
|
1427
2268
|
throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
|
|
@@ -1434,49 +2275,89 @@ var TaskEngine = class {
|
|
|
1434
2275
|
throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
|
|
1435
2276
|
}
|
|
1436
2277
|
const role = task.role ? this.options.roles.get(task.role) : void 0;
|
|
1437
|
-
const running = await this.transition(task, "running");
|
|
1438
2278
|
this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
|
|
1439
|
-
let background =
|
|
2279
|
+
let background = task.context?.background;
|
|
1440
2280
|
if (this.options.memory) {
|
|
1441
2281
|
const memoryContext = await this.options.memory.gatherContext({
|
|
1442
|
-
objective:
|
|
1443
|
-
...
|
|
1444
|
-
...
|
|
2282
|
+
objective: task.objective,
|
|
2283
|
+
...task.role ? { role: task.role } : {},
|
|
2284
|
+
...task.project ? { project: task.project } : {}
|
|
1445
2285
|
});
|
|
1446
2286
|
if (memoryContext.length > 0) {
|
|
1447
2287
|
background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
|
|
1448
2288
|
}
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
});
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
2289
|
+
}
|
|
2290
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
|
|
2291
|
+
const backoffMs = options.retryBackoffMs ?? 0;
|
|
2292
|
+
let current = task;
|
|
2293
|
+
for (let i = 0; i < maxAttempts; i += 1) {
|
|
2294
|
+
const attempt = current.attempt + 1;
|
|
2295
|
+
const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
2296
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2297
|
+
current = await this.transition(current, "running", { attempt });
|
|
2298
|
+
let output;
|
|
2299
|
+
let thrown;
|
|
2300
|
+
try {
|
|
2301
|
+
output = await agent.run({
|
|
2302
|
+
taskId: current.id,
|
|
2303
|
+
objective: current.objective,
|
|
2304
|
+
...role ? { role } : {},
|
|
2305
|
+
...background !== void 0 ? { context: background } : {}
|
|
2306
|
+
});
|
|
2307
|
+
} catch (error) {
|
|
2308
|
+
thrown = error;
|
|
2309
|
+
}
|
|
2310
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2311
|
+
if (thrown !== void 0 || output === void 0) {
|
|
2312
|
+
const outcome = classifyThrown(thrown);
|
|
2313
|
+
const message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
2314
|
+
const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
|
|
2315
|
+
current = await this.transition(current, outcome, {
|
|
2316
|
+
attempt,
|
|
2317
|
+
lastError: message,
|
|
2318
|
+
execution: {
|
|
2319
|
+
executionId,
|
|
2320
|
+
attempt,
|
|
2321
|
+
startedAt,
|
|
2322
|
+
finishedAt,
|
|
2323
|
+
outcome,
|
|
2324
|
+
error: message
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
|
|
2328
|
+
if (retryable) {
|
|
2329
|
+
if (backoffMs > 0) await sleep(backoffMs * attempt);
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
|
|
2333
|
+
return current;
|
|
2334
|
+
}
|
|
2335
|
+
const currentState = await this.options.store.get(current.id);
|
|
2336
|
+
if (currentState?.status === "cancelled") {
|
|
2337
|
+
current = currentState;
|
|
2338
|
+
break;
|
|
2339
|
+
}
|
|
2340
|
+
const result = {
|
|
2341
|
+
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
2342
|
+
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
2343
|
+
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
2344
|
+
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
2345
|
+
...output.error !== void 0 ? { error: output.error } : {},
|
|
2346
|
+
finishedAt
|
|
2347
|
+
};
|
|
2348
|
+
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
2349
|
+
current = await this.transition(current, finalStatus, {
|
|
2350
|
+
attempt,
|
|
2351
|
+
result,
|
|
2352
|
+
execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
|
|
1463
2353
|
});
|
|
2354
|
+
this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
|
|
2355
|
+
break;
|
|
1464
2356
|
}
|
|
1465
|
-
const result = {
|
|
1466
|
-
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
1467
|
-
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
1468
|
-
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
1469
|
-
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
1470
|
-
...output.error !== void 0 ? { error: output.error } : {},
|
|
1471
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1472
|
-
};
|
|
1473
|
-
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
1474
|
-
const finished = await this.transition(running, finalStatus, { result });
|
|
1475
|
-
this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
|
|
1476
2357
|
if (this.options.memory) {
|
|
1477
|
-
await this.options.memory.recordTaskOutcome(
|
|
2358
|
+
await this.options.memory.recordTaskOutcome(current);
|
|
1478
2359
|
}
|
|
1479
|
-
return
|
|
2360
|
+
return current;
|
|
1480
2361
|
}
|
|
1481
2362
|
async cancel(id) {
|
|
1482
2363
|
const task = await this.mustGet(id);
|
|
@@ -1489,13 +2370,14 @@ var TaskEngine = class {
|
|
|
1489
2370
|
}
|
|
1490
2371
|
return this.transition(task, "cancelled");
|
|
1491
2372
|
}
|
|
1492
|
-
/** Error recovery
|
|
2373
|
+
/** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
|
|
1493
2374
|
async retry(id) {
|
|
1494
2375
|
const task = await this.mustGet(id);
|
|
1495
|
-
|
|
2376
|
+
const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
|
|
2377
|
+
if (!retryableStates.includes(task.status)) {
|
|
1496
2378
|
throw new TaskError(
|
|
1497
2379
|
"TASK_INVALID",
|
|
1498
|
-
`Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
|
|
2380
|
+
`Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
|
|
1499
2381
|
);
|
|
1500
2382
|
}
|
|
1501
2383
|
const { result: _discarded, ...rest } = task;
|
|
@@ -1527,10 +2409,14 @@ var TaskEngine = class {
|
|
|
1527
2409
|
if (!allowed.includes(to)) {
|
|
1528
2410
|
throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
|
|
1529
2411
|
}
|
|
2412
|
+
const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
|
|
1530
2413
|
const updated = {
|
|
1531
2414
|
...task,
|
|
1532
2415
|
status: to,
|
|
1533
2416
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2417
|
+
...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
|
|
2418
|
+
...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
|
|
2419
|
+
...executions !== void 0 ? { executions } : {},
|
|
1534
2420
|
...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
|
|
1535
2421
|
};
|
|
1536
2422
|
await this.options.store.save(updated);
|
|
@@ -1551,6 +2437,19 @@ var TaskEngine = class {
|
|
|
1551
2437
|
function generateTaskId() {
|
|
1552
2438
|
return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
1553
2439
|
}
|
|
2440
|
+
function errorCode(error) {
|
|
2441
|
+
return error?.code ?? "";
|
|
2442
|
+
}
|
|
2443
|
+
function classifyThrown(error) {
|
|
2444
|
+
if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
|
|
2445
|
+
const dispatched = error.details?.dispatched;
|
|
2446
|
+
return dispatched === false ? "timed_out" : "unknown";
|
|
2447
|
+
}
|
|
2448
|
+
return "failed";
|
|
2449
|
+
}
|
|
2450
|
+
function sleep(ms) {
|
|
2451
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2452
|
+
}
|
|
1554
2453
|
|
|
1555
2454
|
// src/audit/index.ts
|
|
1556
2455
|
import { appendFile, mkdir as mkdir2 } from "fs/promises";
|
|
@@ -1575,10 +2474,10 @@ var FileAuditLog = class {
|
|
|
1575
2474
|
}
|
|
1576
2475
|
/** Recent events, oldest first; used by the CLI. */
|
|
1577
2476
|
async recent(limit = 20) {
|
|
1578
|
-
const { readFile:
|
|
2477
|
+
const { readFile: readFile2 } = await import("fs/promises");
|
|
1579
2478
|
let raw;
|
|
1580
2479
|
try {
|
|
1581
|
-
raw = await
|
|
2480
|
+
raw = await readFile2(this.filePath, "utf8");
|
|
1582
2481
|
} catch {
|
|
1583
2482
|
return [];
|
|
1584
2483
|
}
|
|
@@ -1593,9 +2492,24 @@ var FileAuditLog = class {
|
|
|
1593
2492
|
}
|
|
1594
2493
|
};
|
|
1595
2494
|
|
|
2495
|
+
// src/memory/base.ts
|
|
2496
|
+
function defaultSearch(provider, text, options = {}) {
|
|
2497
|
+
return provider.retrieve({
|
|
2498
|
+
text,
|
|
2499
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
2500
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
function defaultMetadata(name, options = {}) {
|
|
2504
|
+
return {
|
|
2505
|
+
name,
|
|
2506
|
+
capabilities: options.capabilities ?? ["retrieve", "store", "search"],
|
|
2507
|
+
relevanceRanking: options.relevanceRanking ?? false,
|
|
2508
|
+
...options.backend !== void 0 ? { backend: options.backend } : {}
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
|
|
1596
2512
|
// src/memory/local.ts
|
|
1597
|
-
import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
1598
|
-
import { dirname as dirname3 } from "path";
|
|
1599
2513
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1600
2514
|
"the",
|
|
1601
2515
|
"and",
|
|
@@ -1652,29 +2566,42 @@ function relevanceScore(queryKeywords, entry2) {
|
|
|
1652
2566
|
}
|
|
1653
2567
|
return overlap * 2 + tagMatches * 1.5;
|
|
1654
2568
|
}
|
|
1655
|
-
var LocalMemoryProvider = class {
|
|
2569
|
+
var LocalMemoryProvider = class extends JsonFileStore {
|
|
1656
2570
|
name = "local";
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter(
|
|
1666
|
-
(entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
|
|
1667
|
-
);
|
|
1668
|
-
if (!query.text || query.text.trim().length === 0) {
|
|
1669
|
-
return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
|
|
1670
|
-
}
|
|
1671
|
-
const queryKeywords = keywords(query.text);
|
|
1672
|
-
return filtered.map((entry2) => ({ entry: entry2, score: relevanceScore(queryKeywords, entry2) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry: entry2 }) => entry2);
|
|
2571
|
+
constructor(directory) {
|
|
2572
|
+
super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
|
|
2573
|
+
}
|
|
2574
|
+
metadata() {
|
|
2575
|
+
return defaultMetadata(this.name, {
|
|
2576
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
2577
|
+
relevanceRanking: true,
|
|
2578
|
+
backend: { kind: "json-file", deterministicScoring: true }
|
|
1673
2579
|
});
|
|
1674
2580
|
}
|
|
1675
|
-
|
|
1676
|
-
return this
|
|
1677
|
-
|
|
2581
|
+
search(text, options = {}) {
|
|
2582
|
+
return defaultSearch(this, text, options);
|
|
2583
|
+
}
|
|
2584
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
2585
|
+
query(query) {
|
|
2586
|
+
return this.retrieve(query);
|
|
2587
|
+
}
|
|
2588
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
2589
|
+
write(entry2) {
|
|
2590
|
+
return this.store(entry2);
|
|
2591
|
+
}
|
|
2592
|
+
async retrieve(query) {
|
|
2593
|
+
const entries = await this.readAll();
|
|
2594
|
+
const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter((entry2) => query.category ? entry2.category === query.category : true).filter(
|
|
2595
|
+
(entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
|
|
2596
|
+
);
|
|
2597
|
+
if (!query.text || query.text.trim().length === 0) {
|
|
2598
|
+
return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
|
|
2599
|
+
}
|
|
2600
|
+
const queryKeywords = keywords(query.text);
|
|
2601
|
+
return filtered.map((entry2) => ({ entry: entry2, score: relevanceScore(queryKeywords, entry2) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry: entry2 }) => entry2);
|
|
2602
|
+
}
|
|
2603
|
+
store(entry2) {
|
|
2604
|
+
return this.mutate((entries) => {
|
|
1678
2605
|
const index = entries.findIndex((e) => e.key === entry2.key);
|
|
1679
2606
|
const stamped = {
|
|
1680
2607
|
...entry2,
|
|
@@ -1683,35 +2610,11 @@ var LocalMemoryProvider = class {
|
|
|
1683
2610
|
};
|
|
1684
2611
|
if (index >= 0) entries[index] = stamped;
|
|
1685
2612
|
else entries.push(stamped);
|
|
1686
|
-
|
|
2613
|
+
return [entries, void 0];
|
|
1687
2614
|
});
|
|
1688
2615
|
}
|
|
1689
2616
|
async count() {
|
|
1690
|
-
return (await this.
|
|
1691
|
-
}
|
|
1692
|
-
synchronized(operation) {
|
|
1693
|
-
const result = this.queue.then(operation, operation);
|
|
1694
|
-
this.queue = result.then(
|
|
1695
|
-
() => void 0,
|
|
1696
|
-
() => void 0
|
|
1697
|
-
);
|
|
1698
|
-
return result;
|
|
1699
|
-
}
|
|
1700
|
-
async load() {
|
|
1701
|
-
try {
|
|
1702
|
-
const raw = await readFile2(this.filePath, "utf8");
|
|
1703
|
-
const parsed = JSON.parse(raw);
|
|
1704
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
1705
|
-
} catch {
|
|
1706
|
-
return [];
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
async saveAll(entries) {
|
|
1710
|
-
const tmp = `${this.filePath}.tmp`;
|
|
1711
|
-
await mkdir3(dirname3(this.filePath), { recursive: true });
|
|
1712
|
-
await writeFile2(tmp, `${JSON.stringify(entries, null, 2)}
|
|
1713
|
-
`, "utf8");
|
|
1714
|
-
await rename2(tmp, this.filePath);
|
|
2617
|
+
return (await this.readAll()).length;
|
|
1715
2618
|
}
|
|
1716
2619
|
};
|
|
1717
2620
|
|
|
@@ -1728,11 +2631,40 @@ var RememberaMemoryProvider = class {
|
|
|
1728
2631
|
this.env = config.env ?? process.env;
|
|
1729
2632
|
this.log = log.child({ component: "remembera" });
|
|
1730
2633
|
}
|
|
2634
|
+
get authenticationConfigured() {
|
|
2635
|
+
return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
|
|
2636
|
+
}
|
|
1731
2637
|
headers() {
|
|
1732
2638
|
const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
|
|
1733
2639
|
return key ? { authorization: `Bearer ${key}` } : {};
|
|
1734
2640
|
}
|
|
1735
|
-
|
|
2641
|
+
/** Search delegates to the gateway's relevance query. */
|
|
2642
|
+
search(text, options = {}) {
|
|
2643
|
+
return defaultSearch(
|
|
2644
|
+
this,
|
|
2645
|
+
text,
|
|
2646
|
+
{
|
|
2647
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
2648
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2649
|
+
}
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
metadata() {
|
|
2653
|
+
return defaultMetadata(this.name, {
|
|
2654
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
2655
|
+
relevanceRanking: true,
|
|
2656
|
+
backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
2660
|
+
query(query) {
|
|
2661
|
+
return this.retrieve(query);
|
|
2662
|
+
}
|
|
2663
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
2664
|
+
write(entry2) {
|
|
2665
|
+
return this.store(entry2);
|
|
2666
|
+
}
|
|
2667
|
+
async retrieve(query) {
|
|
1736
2668
|
const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
|
|
1737
2669
|
if (status !== 200) {
|
|
1738
2670
|
throw new MemoryError(`Remembera query returned HTTP ${status}.`);
|
|
@@ -1743,7 +2675,7 @@ var RememberaMemoryProvider = class {
|
|
|
1743
2675
|
}
|
|
1744
2676
|
return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
|
|
1745
2677
|
}
|
|
1746
|
-
async
|
|
2678
|
+
async store(entry2) {
|
|
1747
2679
|
const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
|
|
1748
2680
|
if (status !== 200 && status !== 201) {
|
|
1749
2681
|
throw new MemoryError(`Remembera write returned HTTP ${status}.`);
|
|
@@ -1778,6 +2710,7 @@ var RememberaMemoryProvider = class {
|
|
|
1778
2710
|
content: entry2.content,
|
|
1779
2711
|
scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
|
|
1780
2712
|
...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
|
|
2713
|
+
...typeof entry2.category === "string" ? { category: entry2.category } : {},
|
|
1781
2714
|
...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
|
|
1782
2715
|
...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
|
|
1783
2716
|
};
|
|
@@ -1808,57 +2741,54 @@ function createMemoryProvider(config) {
|
|
|
1808
2741
|
}
|
|
1809
2742
|
|
|
1810
2743
|
// src/memory/service.ts
|
|
2744
|
+
var DEFAULT_ENTRY_LIMIT = 8;
|
|
1811
2745
|
var MAX_CONTEXT_CHARS = 4e3;
|
|
1812
2746
|
var MemoryService = class {
|
|
1813
|
-
constructor(provider, log = logger) {
|
|
2747
|
+
constructor(provider, log = logger, options = {}) {
|
|
1814
2748
|
this.provider = provider;
|
|
1815
2749
|
this.log = log.child({ component: "memory", provider: provider.name });
|
|
2750
|
+
this.required = options.required ?? false;
|
|
2751
|
+
this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
|
|
1816
2752
|
}
|
|
1817
2753
|
provider;
|
|
1818
2754
|
log;
|
|
2755
|
+
required;
|
|
2756
|
+
maxContextChars;
|
|
1819
2757
|
get providerRef() {
|
|
1820
2758
|
return this.provider;
|
|
1821
2759
|
}
|
|
1822
2760
|
get providerName() {
|
|
1823
2761
|
return this.provider.name;
|
|
1824
2762
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
2763
|
+
get isRequired() {
|
|
2764
|
+
return this.required;
|
|
2765
|
+
}
|
|
2766
|
+
/** Structured, budgeted context for a task (§12 — avoid loading all memory). */
|
|
2767
|
+
async contextFor(query) {
|
|
2768
|
+
let entries;
|
|
1827
2769
|
try {
|
|
1828
|
-
|
|
2770
|
+
entries = await this.provider.retrieve({
|
|
1829
2771
|
text: query.objective,
|
|
1830
2772
|
...query.role ? { tags: [query.role] } : {},
|
|
1831
|
-
limit:
|
|
2773
|
+
limit: query.limit ?? DEFAULT_ENTRY_LIMIT
|
|
1832
2774
|
});
|
|
1833
|
-
if (entries.length === 0) return "";
|
|
1834
|
-
const byScope = /* @__PURE__ */ new Map();
|
|
1835
|
-
for (const entry2 of entries) {
|
|
1836
|
-
byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
|
|
1837
|
-
}
|
|
1838
|
-
const parts = [];
|
|
1839
|
-
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
1840
|
-
const scopeEntries = byScope.get(scope) ?? [];
|
|
1841
|
-
if (scopeEntries.length === 0) continue;
|
|
1842
|
-
const lines = scopeEntries.map((entry2) => `- ${entry2.content}`).join("\n");
|
|
1843
|
-
parts.push(`### Relevant memory (${scope})
|
|
1844
|
-
${lines}`);
|
|
1845
|
-
}
|
|
1846
|
-
const context = parts.join("\n\n");
|
|
1847
|
-
return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
|
|
1848
2775
|
} catch (error) {
|
|
1849
|
-
this.
|
|
1850
|
-
`Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
|
|
1851
|
-
);
|
|
1852
|
-
return "";
|
|
2776
|
+
return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
|
|
1853
2777
|
}
|
|
2778
|
+
return this.format(entries);
|
|
2779
|
+
}
|
|
2780
|
+
/** Prompt-ready context text (v1 convenience wrapper). */
|
|
2781
|
+
async gatherContext(query) {
|
|
2782
|
+
return (await this.contextFor(query)).text;
|
|
1854
2783
|
}
|
|
1855
|
-
/** Records a finished task's outcome —
|
|
2784
|
+
/** Records a finished task's outcome — task history plus "known problems" (§12). */
|
|
1856
2785
|
async recordTaskOutcome(task) {
|
|
1857
2786
|
const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
|
|
1858
2787
|
try {
|
|
1859
|
-
await this.provider.
|
|
2788
|
+
await this.provider.store({
|
|
1860
2789
|
key: `task:${task.id}`,
|
|
1861
2790
|
scope: "task",
|
|
2791
|
+
category: "tasks",
|
|
1862
2792
|
content: `Objective: ${task.objective}
|
|
1863
2793
|
Status: ${task.status}
|
|
1864
2794
|
Outcome: ${outcome}`,
|
|
@@ -1869,18 +2799,53 @@ Outcome: ${outcome}`,
|
|
|
1869
2799
|
(report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
|
|
1870
2800
|
);
|
|
1871
2801
|
for (const { report, finding } of significant) {
|
|
1872
|
-
await this.provider.
|
|
2802
|
+
await this.provider.store({
|
|
1873
2803
|
key: `finding:${report.id}:${finding.id}`,
|
|
1874
2804
|
scope: "project",
|
|
2805
|
+
category: "problems",
|
|
1875
2806
|
content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
|
|
1876
2807
|
tags: ["known-problem", report.agent]
|
|
1877
2808
|
});
|
|
1878
2809
|
}
|
|
1879
2810
|
} catch (error) {
|
|
1880
|
-
this.
|
|
1881
|
-
|
|
2811
|
+
this.handleFailure("store", error, void 0);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
format(entries) {
|
|
2815
|
+
if (entries.length === 0) return { text: "", entries, truncated: false };
|
|
2816
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
2817
|
+
for (const entry2 of entries) {
|
|
2818
|
+
byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
|
|
2819
|
+
}
|
|
2820
|
+
const parts = [];
|
|
2821
|
+
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
2822
|
+
const scopeEntries = byScope.get(scope) ?? [];
|
|
2823
|
+
if (scopeEntries.length === 0) continue;
|
|
2824
|
+
const lines = scopeEntries.map((entry2) => {
|
|
2825
|
+
const category = entry2.category ? `[${entry2.category}] ` : "";
|
|
2826
|
+
return `- ${category}${entry2.content}`;
|
|
2827
|
+
}).join("\n");
|
|
2828
|
+
parts.push(`### Relevant memory (${scope})
|
|
2829
|
+
${lines}`);
|
|
2830
|
+
}
|
|
2831
|
+
let text = parts.join("\n\n");
|
|
2832
|
+
const truncated = text.length > this.maxContextChars;
|
|
2833
|
+
if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
|
|
2834
|
+
return { text, entries, truncated };
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* Failure policy (§12): continue unless memory is explicitly required.
|
|
2838
|
+
*/
|
|
2839
|
+
handleFailure(operation, error, fallback) {
|
|
2840
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2841
|
+
if (this.required) {
|
|
2842
|
+
throw new MemoryError(
|
|
2843
|
+
`Memory is marked required, but ${operation} failed: ${message}`,
|
|
2844
|
+
{ cause: error }
|
|
1882
2845
|
);
|
|
1883
2846
|
}
|
|
2847
|
+
this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
|
|
2848
|
+
return fallback ?? { text: "", entries: [], truncated: false };
|
|
1884
2849
|
}
|
|
1885
2850
|
};
|
|
1886
2851
|
|
|
@@ -1914,7 +2879,11 @@ function createTaskEngine(config, log = logger, directory) {
|
|
|
1914
2879
|
agents,
|
|
1915
2880
|
roles,
|
|
1916
2881
|
store: new FileTaskStore(directory),
|
|
1917
|
-
...config.project.memory !== void 0 ? {
|
|
2882
|
+
...config.project.memory !== void 0 ? {
|
|
2883
|
+
memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
|
|
2884
|
+
required: config.project.memory.required
|
|
2885
|
+
})
|
|
2886
|
+
} : {},
|
|
1918
2887
|
audit: new FileAuditLog(),
|
|
1919
2888
|
log
|
|
1920
2889
|
});
|
|
@@ -1981,7 +2950,7 @@ function registerTaskCommands(program) {
|
|
|
1981
2950
|
const cancelled = await engine.cancel(taskId);
|
|
1982
2951
|
console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
|
|
1983
2952
|
});
|
|
1984
|
-
task.command("retry <task-id>").description("Reset a failed
|
|
2953
|
+
task.command("retry <task-id>").description("Reset a failed, timed out, unknown or cancelled task to created; combine with --run to execute it again.").option("--run", "Run the task again immediately after resetting it.").action(async (taskId, options) => {
|
|
1985
2954
|
const config = loadConfig();
|
|
1986
2955
|
const engine = createTaskEngine(config);
|
|
1987
2956
|
const reset = await engine.retry(taskId);
|
|
@@ -2004,6 +2973,15 @@ function printOutcome(task, options = {}) {
|
|
|
2004
2973
|
if (task.context?.files) console.log(` files: ${task.context.files.join(", ")}`);
|
|
2005
2974
|
console.log(` objective: ${task.objective}`);
|
|
2006
2975
|
}
|
|
2976
|
+
if (options.verbose && task.attempt > 0) {
|
|
2977
|
+
console.log(` attempt: ${task.attempt}`);
|
|
2978
|
+
}
|
|
2979
|
+
if (task.lastError) console.log(` error: ${task.lastError}`);
|
|
2980
|
+
if (options.verbose && task.executions && task.executions.length > 0) {
|
|
2981
|
+
console.log(
|
|
2982
|
+
` executions: ${task.executions.map((e) => `#${e.attempt}:${e.outcome}`).join(", ")}`
|
|
2983
|
+
);
|
|
2984
|
+
}
|
|
2007
2985
|
const result = task.result;
|
|
2008
2986
|
if (!result) return;
|
|
2009
2987
|
if (result.summary) console.log(` summary: ${result.summary}`);
|
|
@@ -2019,6 +2997,87 @@ function truncate2(text, max) {
|
|
|
2019
2997
|
|
|
2020
2998
|
// src/reports/aggregate.ts
|
|
2021
2999
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
3000
|
+
|
|
3001
|
+
// src/reports/schema.ts
|
|
3002
|
+
import { z as z2 } from "zod";
|
|
3003
|
+
var evidenceSchema = z2.union([
|
|
3004
|
+
z2.string().transform((excerpt) => ({ kind: "observation", excerpt })),
|
|
3005
|
+
z2.object({
|
|
3006
|
+
kind: z2.enum(["file", "line", "url", "command", "observation", "artifact"]),
|
|
3007
|
+
ref: z2.string().optional(),
|
|
3008
|
+
line: z2.number().int().optional(),
|
|
3009
|
+
excerpt: z2.string().optional(),
|
|
3010
|
+
agent: z2.string().optional()
|
|
3011
|
+
})
|
|
3012
|
+
]);
|
|
3013
|
+
var findingSchema = z2.object({
|
|
3014
|
+
id: z2.string().min(1),
|
|
3015
|
+
title: z2.string().min(1),
|
|
3016
|
+
detail: z2.string().default(""),
|
|
3017
|
+
description: z2.string().optional(),
|
|
3018
|
+
severity: z2.enum(["info", "low", "medium", "high", "critical"]).optional(),
|
|
3019
|
+
confidence: z2.enum(["low", "medium", "high"]).optional(),
|
|
3020
|
+
severityBySource: z2.record(z2.enum(["info", "low", "medium", "high", "critical"])).optional(),
|
|
3021
|
+
confidenceBySource: z2.record(z2.enum(["low", "medium", "high"])).optional(),
|
|
3022
|
+
recommendations: z2.array(z2.string()).optional(),
|
|
3023
|
+
evidence: z2.array(evidenceSchema).optional(),
|
|
3024
|
+
location: z2.string().optional(),
|
|
3025
|
+
tags: z2.array(z2.string()).optional(),
|
|
3026
|
+
source: z2.object({ agent: z2.string().optional(), reportId: z2.string().optional(), pipelineRunId: z2.string().optional() }).optional(),
|
|
3027
|
+
recommendation: z2.string().optional()
|
|
3028
|
+
});
|
|
3029
|
+
var artifactSchema = z2.object({
|
|
3030
|
+
kind: z2.enum(["file", "patch", "log", "screenshot", "document", "data"]),
|
|
3031
|
+
ref: z2.string().min(1),
|
|
3032
|
+
description: z2.string().optional(),
|
|
3033
|
+
mediaType: z2.string().optional(),
|
|
3034
|
+
bytes: z2.number().int().optional()
|
|
3035
|
+
});
|
|
3036
|
+
var reportSchema = z2.object({
|
|
3037
|
+
id: z2.string().min(1),
|
|
3038
|
+
taskId: z2.string().min(1),
|
|
3039
|
+
agent: z2.string().min(1),
|
|
3040
|
+
agentId: z2.string().optional(),
|
|
3041
|
+
pipelineRunId: z2.string().optional(),
|
|
3042
|
+
model: z2.string().optional(),
|
|
3043
|
+
summary: z2.string(),
|
|
3044
|
+
findings: z2.array(findingSchema),
|
|
3045
|
+
recommendations: z2.array(z2.string()),
|
|
3046
|
+
confidence: z2.enum(["low", "medium", "high"]).optional(),
|
|
3047
|
+
evidence: z2.array(evidenceSchema).optional(),
|
|
3048
|
+
artifacts: z2.array(artifactSchema).optional(),
|
|
3049
|
+
createdAt: z2.string().min(1),
|
|
3050
|
+
metadata: z2.record(z2.unknown()).optional()
|
|
3051
|
+
});
|
|
3052
|
+
function validateReport(value) {
|
|
3053
|
+
const parsed = reportSchema.safeParse(value);
|
|
3054
|
+
if (!parsed.success) {
|
|
3055
|
+
const issues = parsed.error.issues.map((issue) => ({
|
|
3056
|
+
path: issue.path.join(".") || "(root)",
|
|
3057
|
+
message: issue.message
|
|
3058
|
+
}));
|
|
3059
|
+
return {
|
|
3060
|
+
valid: false,
|
|
3061
|
+
issues,
|
|
3062
|
+
reason: `report failed schema validation (${issues.length} issue(s))`
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
const report = parsed.data;
|
|
3066
|
+
report.findings = report.findings.map((finding) => ({
|
|
3067
|
+
...finding,
|
|
3068
|
+
description: finding.description ?? finding.detail,
|
|
3069
|
+
...finding.source === void 0 ? { source: { agent: report.agent, reportId: report.id, ...report.pipelineRunId !== void 0 ? { pipelineRunId: report.pipelineRunId } : {} } } : {}
|
|
3070
|
+
}));
|
|
3071
|
+
return { valid: true, report };
|
|
3072
|
+
}
|
|
3073
|
+
function normalizeEvidence(input) {
|
|
3074
|
+
if (input === void 0) return void 0;
|
|
3075
|
+
return input.map(
|
|
3076
|
+
(item) => typeof item === "string" ? { kind: "observation", excerpt: item } : item
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
// src/reports/aggregate.ts
|
|
2022
3081
|
var SEVERITY_RANK = {
|
|
2023
3082
|
info: 0,
|
|
2024
3083
|
low: 1,
|
|
@@ -2040,7 +3099,12 @@ function aggregateReports(reports, options = {}) {
|
|
|
2040
3099
|
}
|
|
2041
3100
|
const sources = [...new Set(reports.map((r) => r.agent))].sort();
|
|
2042
3101
|
const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
|
|
2043
|
-
const
|
|
3102
|
+
const conflicts = detectConflicts(findingGroups);
|
|
3103
|
+
const agreements = findingGroups.filter(
|
|
3104
|
+
(group) => group.sources.length > 1 && group.severity !== void 0 && new Set(Object.values(group.severityBySource ?? {})).size === 1
|
|
3105
|
+
);
|
|
3106
|
+
const disputed = conflicts.length;
|
|
3107
|
+
const summary = `${reports.length} report(s) from ${sources.length > 0 ? sources.join(", ") : "no agents"}: ${findingGroups.length} unique finding group(s) (${totalRawFindings} raw), ${agreements.length} agreement(s), ${disputed} conflict(s), ${recommendationGroups.length} recommendation group(s).`;
|
|
2044
3108
|
return {
|
|
2045
3109
|
id: `combined-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
|
|
2046
3110
|
title: options.objective !== void 0 ? truncate3(options.objective, 120) : "Combined report",
|
|
@@ -2049,10 +3113,57 @@ function aggregateReports(reports, options = {}) {
|
|
|
2049
3113
|
taskIds: options.taskIds ?? [],
|
|
2050
3114
|
summary,
|
|
2051
3115
|
findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
|
|
3116
|
+
agreements,
|
|
3117
|
+
conflicts,
|
|
2052
3118
|
recommendations: recommendationGroups,
|
|
2053
3119
|
sources,
|
|
2054
3120
|
reportIds: reports.map((r) => r.id),
|
|
2055
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3121
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3122
|
+
metadata: {
|
|
3123
|
+
agents: sources.length,
|
|
3124
|
+
reports: reports.length,
|
|
3125
|
+
rawFindings: totalRawFindings,
|
|
3126
|
+
findingGroups: findingGroups.length,
|
|
3127
|
+
agreements: agreements.length,
|
|
3128
|
+
conflicts: conflicts.length
|
|
3129
|
+
}
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
function rankConfidence(confidence) {
|
|
3133
|
+
return confidence ? { low: 1, medium: 2, high: 3 }[confidence] : 0;
|
|
3134
|
+
}
|
|
3135
|
+
function detectConflicts(groups) {
|
|
3136
|
+
const conflicts = [];
|
|
3137
|
+
for (const group of groups) {
|
|
3138
|
+
if (group.sources.length < 2) continue;
|
|
3139
|
+
const severities = new Set(Object.values(group.severityBySource ?? {}));
|
|
3140
|
+
if (severities.size > 1) {
|
|
3141
|
+
conflicts.push({
|
|
3142
|
+
type: "severity",
|
|
3143
|
+
findingTitle: group.title,
|
|
3144
|
+
positions: group.sources.map((source) => position(source, group)),
|
|
3145
|
+
note: `Agents disagree on severity (${[...severities].sort((a, b) => SEVERITY_RANK[a] - SEVERITY_RANK[b]).join(", ")}); the highest severity is shown, every position is preserved.`
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
const recommendations = (group.recommendations ?? []).filter((r) => r.length > 0);
|
|
3149
|
+
if (recommendations.length > 1) {
|
|
3150
|
+
conflicts.push({
|
|
3151
|
+
type: "recommendation",
|
|
3152
|
+
findingTitle: group.title,
|
|
3153
|
+
positions: group.sources.map((source) => position(source, group)),
|
|
3154
|
+
note: `Agents proposed ${recommendations.length} different fixes; none was discarded.`
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
return conflicts;
|
|
3159
|
+
}
|
|
3160
|
+
function position(source, group) {
|
|
3161
|
+
return {
|
|
3162
|
+
agent: source.agent,
|
|
3163
|
+
reportId: source.reportId,
|
|
3164
|
+
...group.severityBySource?.[source.agent] !== void 0 ? { severity: group.severityBySource[source.agent] } : {},
|
|
3165
|
+
...group.confidenceBySource?.[source.agent] !== void 0 ? { confidence: group.confidenceBySource[source.agent] } : {},
|
|
3166
|
+
...group.recommendations?.[0] !== void 0 ? { recommendation: group.recommendations[0] } : {}
|
|
2056
3167
|
};
|
|
2057
3168
|
}
|
|
2058
3169
|
function rank(finding) {
|
|
@@ -2065,10 +3176,22 @@ function mergeFinding(groups, finding, report, threshold) {
|
|
|
2065
3176
|
group.occurrences += 1;
|
|
2066
3177
|
group.details.push(finding.detail);
|
|
2067
3178
|
group.sources.push(source);
|
|
2068
|
-
group.evidence.push(...finding.evidence ?? []);
|
|
3179
|
+
group.evidence.push(...normalizeEvidence(finding.evidence) ?? []);
|
|
2069
3180
|
if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
|
|
2070
3181
|
group.severity = finding.severity;
|
|
2071
3182
|
}
|
|
3183
|
+
if (finding.confidence && rankConfidence(finding.confidence) > rankConfidence(group.confidence)) {
|
|
3184
|
+
group.confidence = finding.confidence;
|
|
3185
|
+
}
|
|
3186
|
+
if (finding.severity) {
|
|
3187
|
+
group.severityBySource = { ...group.severityBySource, [report.agent]: finding.severity };
|
|
3188
|
+
}
|
|
3189
|
+
if (finding.confidence) {
|
|
3190
|
+
group.confidenceBySource = { ...group.confidenceBySource, [report.agent]: finding.confidence };
|
|
3191
|
+
}
|
|
3192
|
+
if (finding.recommendation && !group.recommendations?.includes(finding.recommendation)) {
|
|
3193
|
+
group.recommendations = [...group.recommendations ?? [], finding.recommendation];
|
|
3194
|
+
}
|
|
2072
3195
|
return;
|
|
2073
3196
|
}
|
|
2074
3197
|
groups.push({
|
|
@@ -2077,7 +3200,11 @@ function mergeFinding(groups, finding, report, threshold) {
|
|
|
2077
3200
|
sources: [source],
|
|
2078
3201
|
occurrences: 1,
|
|
2079
3202
|
...finding.severity ? { severity: finding.severity } : {},
|
|
2080
|
-
|
|
3203
|
+
...finding.confidence ? { confidence: finding.confidence } : {},
|
|
3204
|
+
...finding.severity ? { severityBySource: { [report.agent]: finding.severity } } : {},
|
|
3205
|
+
...finding.confidence ? { confidenceBySource: { [report.agent]: finding.confidence } } : {},
|
|
3206
|
+
...finding.recommendation ? { recommendations: [finding.recommendation] } : {},
|
|
3207
|
+
evidence: [...normalizeEvidence(finding.evidence) ?? []]
|
|
2081
3208
|
});
|
|
2082
3209
|
}
|
|
2083
3210
|
function mergeRecommendation(groups, text, report, threshold) {
|
|
@@ -2159,6 +3286,7 @@ function extractReportFromText(raw, meta) {
|
|
|
2159
3286
|
id: `${meta.taskId}-f-${index + 1}`,
|
|
2160
3287
|
title: truncate4(text, 120),
|
|
2161
3288
|
detail: text,
|
|
3289
|
+
description: text,
|
|
2162
3290
|
severity: guessSeverity(text)
|
|
2163
3291
|
}));
|
|
2164
3292
|
return {
|
|
@@ -2169,7 +3297,8 @@ function extractReportFromText(raw, meta) {
|
|
|
2169
3297
|
summary,
|
|
2170
3298
|
findings,
|
|
2171
3299
|
recommendations: recommendationTexts.map((t) => truncate4(t, 200)),
|
|
2172
|
-
|
|
3300
|
+
...evidenceTexts.length > 0 ? { evidence: evidenceTexts.map((excerpt) => ({ kind: "observation", excerpt })) } : {},
|
|
3301
|
+
metadata: evidenceTexts.length > 0 ? { evidenceCount: evidenceTexts.length } : void 0,
|
|
2173
3302
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2174
3303
|
};
|
|
2175
3304
|
}
|
|
@@ -2185,27 +3314,46 @@ var ReportService = class {
|
|
|
2185
3314
|
}
|
|
2186
3315
|
store;
|
|
2187
3316
|
log;
|
|
3317
|
+
/** Rejected reports from the last collection (§13 — malformed never enters aggregation). */
|
|
3318
|
+
lastDiagnostics = { collected: 0, accepted: 0, rejected: [] };
|
|
2188
3319
|
get storeRef() {
|
|
2189
3320
|
return this.store;
|
|
2190
3321
|
}
|
|
2191
|
-
/** Normalizes finished tasks into reports (§17 — normalizer)
|
|
3322
|
+
/** Normalizes finished tasks into reports (§17 — normalizer), validating each
|
|
3323
|
+
* one so malformed reports never reach aggregation (§13). */
|
|
2192
3324
|
async collectFromTasks(tasks) {
|
|
2193
|
-
const
|
|
3325
|
+
const candidates = [];
|
|
2194
3326
|
for (const task of tasks) {
|
|
2195
3327
|
const structured = task.result?.reports ?? [];
|
|
2196
3328
|
if (structured.length > 0) {
|
|
2197
|
-
|
|
3329
|
+
candidates.push(...structured);
|
|
2198
3330
|
continue;
|
|
2199
3331
|
}
|
|
2200
3332
|
const text = task.result?.rawOutput ?? task.result?.summary ?? "";
|
|
2201
3333
|
if (text.trim().length === 0) continue;
|
|
2202
|
-
|
|
3334
|
+
candidates.push(
|
|
2203
3335
|
extractReportFromText(text, {
|
|
2204
3336
|
taskId: task.id,
|
|
2205
3337
|
agent: task.agent ?? "unknown"
|
|
2206
3338
|
})
|
|
2207
3339
|
);
|
|
2208
3340
|
}
|
|
3341
|
+
const reports = [];
|
|
3342
|
+
const rejected = [];
|
|
3343
|
+
for (const candidate of candidates) {
|
|
3344
|
+
const result = validateReport(candidate);
|
|
3345
|
+
if (result.valid) {
|
|
3346
|
+
reports.push(result.report);
|
|
3347
|
+
} else {
|
|
3348
|
+
rejected.push({
|
|
3349
|
+
key: candidate?.id ?? "(unknown)",
|
|
3350
|
+
reason: result.reason,
|
|
3351
|
+
issues: result.issues.map((issue) => `${issue.path}: ${issue.message}`)
|
|
3352
|
+
});
|
|
3353
|
+
this.log.warn(`Rejected malformed report: ${result.reason}`);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
this.lastDiagnostics = { collected: candidates.length, accepted: reports.length, rejected };
|
|
2209
3357
|
return reports;
|
|
2210
3358
|
}
|
|
2211
3359
|
async saveReports(reports) {
|
|
@@ -2228,84 +3376,39 @@ var ReportService = class {
|
|
|
2228
3376
|
};
|
|
2229
3377
|
|
|
2230
3378
|
// src/reports/store.ts
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
filePath;
|
|
2235
|
-
queue = Promise.resolve();
|
|
2236
|
-
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
2237
|
-
this.filePath = `${directory}/reports.json`;
|
|
3379
|
+
var FileReportStore = class extends JsonFileStore {
|
|
3380
|
+
constructor(directory) {
|
|
3381
|
+
super({ ...directory !== void 0 ? { directory } : {}, fileName: "reports.json" });
|
|
2238
3382
|
}
|
|
2239
3383
|
async saveReports(reports) {
|
|
2240
|
-
|
|
2241
|
-
const entries = await this.load();
|
|
3384
|
+
await this.mutate((entries) => {
|
|
2242
3385
|
for (const report of reports) {
|
|
2243
|
-
const index = entries.findIndex(
|
|
2244
|
-
(e) => e.kind === "report" && e.report.id === report.id
|
|
2245
|
-
);
|
|
3386
|
+
const index = entries.findIndex((e) => e.kind === "report" && e.report.id === report.id);
|
|
2246
3387
|
const entry2 = { kind: "report", report };
|
|
2247
3388
|
if (index >= 0) entries[index] = entry2;
|
|
2248
3389
|
else entries.push(entry2);
|
|
2249
3390
|
}
|
|
2250
|
-
|
|
3391
|
+
return [entries, void 0];
|
|
2251
3392
|
});
|
|
2252
3393
|
}
|
|
2253
3394
|
async listReports(filter = {}) {
|
|
2254
|
-
return this.
|
|
2255
|
-
const entries = await this.load();
|
|
2256
|
-
return entries.filter((e) => e.kind === "report").map((e) => e.report).filter((report) => filter.taskId ? report.taskId === filter.taskId : true).filter((report) => filter.agent ? report.agent === filter.agent : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
2257
|
-
});
|
|
3395
|
+
return (await this.readAll()).filter((e) => e.kind === "report").map((e) => e.report).filter((report) => filter.taskId ? report.taskId === filter.taskId : true).filter((report) => filter.agent ? report.agent === filter.agent : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
2258
3396
|
}
|
|
2259
3397
|
async saveCombined(combined) {
|
|
2260
|
-
|
|
2261
|
-
const
|
|
2262
|
-
const index = entries.findIndex(
|
|
2263
|
-
(e) => e.kind === "combined" && e.combined.id === combined.id
|
|
2264
|
-
);
|
|
3398
|
+
await this.mutate((entries) => {
|
|
3399
|
+
const index = entries.findIndex((e) => e.kind === "combined" && e.combined.id === combined.id);
|
|
2265
3400
|
const entry2 = { kind: "combined", combined };
|
|
2266
3401
|
if (index >= 0) entries[index] = entry2;
|
|
2267
3402
|
else entries.push(entry2);
|
|
2268
|
-
|
|
3403
|
+
return [entries, void 0];
|
|
2269
3404
|
});
|
|
2270
3405
|
}
|
|
2271
3406
|
async getCombined(id) {
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
const found = entries.find((e) => e.kind === "combined" && e.combined.id === id);
|
|
2275
|
-
return found?.kind === "combined" ? found.combined : void 0;
|
|
2276
|
-
});
|
|
3407
|
+
const found = (await this.readAll()).find((e) => e.kind === "combined" && e.combined.id === id);
|
|
3408
|
+
return found?.kind === "combined" ? found.combined : void 0;
|
|
2277
3409
|
}
|
|
2278
3410
|
async listCombined(filter = {}) {
|
|
2279
|
-
return this.
|
|
2280
|
-
const entries = await this.load();
|
|
2281
|
-
return entries.filter((e) => e.kind === "combined").map((e) => e.combined).filter(
|
|
2282
|
-
(combined) => filter.pipelineRunId ? combined.pipelineRunId === filter.pipelineRunId : true
|
|
2283
|
-
).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
2284
|
-
});
|
|
2285
|
-
}
|
|
2286
|
-
synchronized(operation) {
|
|
2287
|
-
const result = this.queue.then(operation, operation);
|
|
2288
|
-
this.queue = result.then(
|
|
2289
|
-
() => void 0,
|
|
2290
|
-
() => void 0
|
|
2291
|
-
);
|
|
2292
|
-
return result;
|
|
2293
|
-
}
|
|
2294
|
-
async load() {
|
|
2295
|
-
try {
|
|
2296
|
-
const raw = await readFile3(this.filePath, "utf8");
|
|
2297
|
-
const parsed = JSON.parse(raw);
|
|
2298
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
2299
|
-
} catch {
|
|
2300
|
-
return [];
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
async write(entries) {
|
|
2304
|
-
const tmp = `${this.filePath}.tmp`;
|
|
2305
|
-
await mkdir4(dirname4(this.filePath), { recursive: true });
|
|
2306
|
-
await writeFile3(tmp, `${JSON.stringify(entries, null, 2)}
|
|
2307
|
-
`, "utf8");
|
|
2308
|
-
await rename3(tmp, this.filePath);
|
|
3411
|
+
return (await this.readAll()).filter((e) => e.kind === "combined").map((e) => e.combined).filter((combined) => filter.pipelineRunId ? combined.pipelineRunId === filter.pipelineRunId : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
2309
3412
|
}
|
|
2310
3413
|
};
|
|
2311
3414
|
|
|
@@ -2339,8 +3442,9 @@ function registerReportCommands(program) {
|
|
|
2339
3442
|
return;
|
|
2340
3443
|
}
|
|
2341
3444
|
for (const c of combined) {
|
|
3445
|
+
const conflicts = c.conflicts.length > 0 ? ` ${c.conflicts.length} CONFLICT(S)` : "";
|
|
2342
3446
|
console.log(
|
|
2343
|
-
`${c.id} ${c.findings.length} finding(s) from ${c.sources.length} source(s)${c.pipelineRunId ? ` run=${c.pipelineRunId}` : ""}`
|
|
3447
|
+
`${c.id} ${c.findings.length} finding(s) from ${c.sources.length} source(s)${conflicts}${c.pipelineRunId ? ` run=${c.pipelineRunId}` : ""}`
|
|
2344
3448
|
);
|
|
2345
3449
|
}
|
|
2346
3450
|
});
|
|
@@ -2377,6 +3481,21 @@ function registerReportCommands(program) {
|
|
|
2377
3481
|
console.log(` reports: ${combined.reportIds.length}`);
|
|
2378
3482
|
if (combined.pipelineRunId) console.log(` run: ${combined.pipelineRunId}`);
|
|
2379
3483
|
console.log(` summary: ${combined.summary}`);
|
|
3484
|
+
if (combined.conflicts.length > 0) {
|
|
3485
|
+
console.log(` CONFLICTS (${combined.conflicts.length}) \u2014 both positions preserved:`);
|
|
3486
|
+
for (const conflict of combined.conflicts) {
|
|
3487
|
+
console.log(` ! ${conflict.findingTitle} [${conflict.type}]`);
|
|
3488
|
+
for (const position2 of conflict.positions) {
|
|
3489
|
+
console.log(
|
|
3490
|
+
` ${position2.agent}: severity=${position2.severity ?? "unstated"}${position2.recommendation ? ` fix="${position2.recommendation}"` : ""}`
|
|
3491
|
+
);
|
|
3492
|
+
}
|
|
3493
|
+
console.log(` ${conflict.note}`);
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
if (combined.agreements.length > 0) {
|
|
3497
|
+
console.log(` agreements: ${combined.agreements.length} (multi-agent consensus)`);
|
|
3498
|
+
}
|
|
2380
3499
|
if (combined.findings.length > 0) {
|
|
2381
3500
|
console.log(" findings (grouped, duplicates merged):");
|
|
2382
3501
|
for (const finding of combined.findings) {
|
|
@@ -2470,7 +3589,7 @@ function truncate5(text, max) {
|
|
|
2470
3589
|
|
|
2471
3590
|
// src/planner/model.ts
|
|
2472
3591
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
2473
|
-
import { z as
|
|
3592
|
+
import { z as z3 } from "zod";
|
|
2474
3593
|
|
|
2475
3594
|
// src/planner/context.ts
|
|
2476
3595
|
function buildPlannerContext(request) {
|
|
@@ -2523,21 +3642,21 @@ ${request.context}`);
|
|
|
2523
3642
|
}
|
|
2524
3643
|
|
|
2525
3644
|
// src/planner/model.ts
|
|
2526
|
-
var planStepJsonSchema =
|
|
2527
|
-
id:
|
|
2528
|
-
title:
|
|
2529
|
-
description:
|
|
2530
|
-
order:
|
|
2531
|
-
targets:
|
|
2532
|
-
acceptance_criteria:
|
|
2533
|
-
depends_on:
|
|
2534
|
-
source_findings:
|
|
3645
|
+
var planStepJsonSchema = z3.object({
|
|
3646
|
+
id: z3.string().optional(),
|
|
3647
|
+
title: z3.string().min(1),
|
|
3648
|
+
description: z3.string().optional(),
|
|
3649
|
+
order: z3.number().int().optional(),
|
|
3650
|
+
targets: z3.array(z3.string()).optional(),
|
|
3651
|
+
acceptance_criteria: z3.array(z3.string()).optional(),
|
|
3652
|
+
depends_on: z3.array(z3.string()).optional(),
|
|
3653
|
+
source_findings: z3.array(z3.string()).optional()
|
|
2535
3654
|
}).strict();
|
|
2536
|
-
var planJsonSchema =
|
|
2537
|
-
summary:
|
|
2538
|
-
steps:
|
|
2539
|
-
risks:
|
|
2540
|
-
notes:
|
|
3655
|
+
var planJsonSchema = z3.object({
|
|
3656
|
+
summary: z3.string().min(1),
|
|
3657
|
+
steps: z3.array(planStepJsonSchema).min(1),
|
|
3658
|
+
risks: z3.array(z3.string()).optional(),
|
|
3659
|
+
notes: z3.array(z3.string()).optional()
|
|
2541
3660
|
}).strict();
|
|
2542
3661
|
var ModelPlanner = class {
|
|
2543
3662
|
constructor(options) {
|
|
@@ -2630,9 +3749,23 @@ var PipelineEngine = class {
|
|
|
2630
3749
|
}
|
|
2631
3750
|
options;
|
|
2632
3751
|
log;
|
|
3752
|
+
/** Runs executing in THIS process — cancellation can terminate them directly. */
|
|
3753
|
+
activeRuns = /* @__PURE__ */ new Map();
|
|
2633
3754
|
get store() {
|
|
2634
3755
|
return this.options.store;
|
|
2635
3756
|
}
|
|
3757
|
+
/**
|
|
3758
|
+
* Saves the run while preserving fields that may be set by another process
|
|
3759
|
+
* (e.g. a cancellation request) — an in-memory snapshot must never clobber
|
|
3760
|
+
* externally requested cancellation (§9.2).
|
|
3761
|
+
*/
|
|
3762
|
+
async persist(run2) {
|
|
3763
|
+
const current = await this.options.store.get(run2.id);
|
|
3764
|
+
await this.options.store.save({
|
|
3765
|
+
...run2,
|
|
3766
|
+
...current?.cancellationRequested === true ? { cancellationRequested: true, cancellationRequestedAt: current.cancellationRequestedAt } : {}
|
|
3767
|
+
});
|
|
3768
|
+
}
|
|
2636
3769
|
/** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
|
|
2637
3770
|
validate(def) {
|
|
2638
3771
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -2732,18 +3865,44 @@ var PipelineEngine = class {
|
|
|
2732
3865
|
);
|
|
2733
3866
|
}
|
|
2734
3867
|
}
|
|
3868
|
+
if (step.model) {
|
|
3869
|
+
const separator = step.model.indexOf(":");
|
|
3870
|
+
if (separator <= 0 || separator === step.model.length - 1) {
|
|
3871
|
+
throw new PipelineError(
|
|
3872
|
+
"PIPELINE_INVALID",
|
|
3873
|
+
`Step "${step.id}" has an invalid model reference "${step.model}" \u2014 use "provider:model-id".`
|
|
3874
|
+
);
|
|
3875
|
+
}
|
|
3876
|
+
const providerName = step.model.slice(0, separator);
|
|
3877
|
+
if (!this.options.providers.some((p) => p.name === providerName)) {
|
|
3878
|
+
throw new PipelineError(
|
|
3879
|
+
"PIPELINE_INVALID",
|
|
3880
|
+
`Step "${step.id}" uses model "${step.model}" but provider "${providerName}" is not configured.`
|
|
3881
|
+
);
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
2735
3884
|
}
|
|
2736
3885
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2737
3886
|
const run2 = {
|
|
2738
3887
|
id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
|
|
2739
3888
|
pipelineId: def.id,
|
|
3889
|
+
attempt: 1,
|
|
2740
3890
|
objective,
|
|
2741
3891
|
status: "running",
|
|
2742
3892
|
startedAt: now,
|
|
2743
3893
|
stepRuns: []
|
|
2744
3894
|
};
|
|
2745
3895
|
await this.options.store.save(run2);
|
|
2746
|
-
|
|
3896
|
+
const liveTaskIds = /* @__PURE__ */ new Set();
|
|
3897
|
+
this.activeRuns.set(run2.id, {
|
|
3898
|
+
taskIds: liveTaskIds,
|
|
3899
|
+
cancelTasks: async () => {
|
|
3900
|
+
for (const taskId of liveTaskIds) {
|
|
3901
|
+
await this.options.tasks.cancel(taskId).catch(() => void 0);
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
});
|
|
3905
|
+
this.log.info(`Running pipeline ${def.id} as ${run2.id} (attempt ${run2.attempt}).`);
|
|
2747
3906
|
await this.options.audit?.record({
|
|
2748
3907
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2749
3908
|
action: "pipeline.run.started",
|
|
@@ -2755,7 +3914,22 @@ var PipelineEngine = class {
|
|
|
2755
3914
|
const pending = new Set(def.steps.map((s) => s.id));
|
|
2756
3915
|
const contextParts = [];
|
|
2757
3916
|
const allReports = [];
|
|
3917
|
+
let cancelled = false;
|
|
2758
3918
|
while (pending.size > 0) {
|
|
3919
|
+
const persisted = await this.options.store.get(run2.id);
|
|
3920
|
+
if (persisted?.cancellationRequested === true) {
|
|
3921
|
+
cancelled = true;
|
|
3922
|
+
run2.cancellationRequested = true;
|
|
3923
|
+
run2.cancellationRequestedAt = persisted.cancellationRequestedAt;
|
|
3924
|
+
await this.activeRuns.get(run2.id)?.cancelTasks();
|
|
3925
|
+
for (const step of def.steps.filter((s) => pending.has(s.id))) {
|
|
3926
|
+
const stepRun = { stepId: step.id, status: "cancelled" };
|
|
3927
|
+
run2.stepRuns.push(stepRun);
|
|
3928
|
+
options.onStep?.(stepRun);
|
|
3929
|
+
}
|
|
3930
|
+
pending.clear();
|
|
3931
|
+
break;
|
|
3932
|
+
}
|
|
2759
3933
|
const ready = def.steps.filter(
|
|
2760
3934
|
(step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
|
|
2761
3935
|
);
|
|
@@ -2772,7 +3946,8 @@ var PipelineEngine = class {
|
|
|
2772
3946
|
...role ? { role } : {},
|
|
2773
3947
|
combinedContext: contextParts.join("\n\n"),
|
|
2774
3948
|
reports: [...allReports],
|
|
2775
|
-
pipelineRunId: run2.id
|
|
3949
|
+
pipelineRunId: run2.id,
|
|
3950
|
+
runId: run2.id
|
|
2776
3951
|
};
|
|
2777
3952
|
return this.executeStep(step, ctx);
|
|
2778
3953
|
})
|
|
@@ -2789,7 +3964,9 @@ ${contribution}`);
|
|
|
2789
3964
|
}
|
|
2790
3965
|
const stepRun = {
|
|
2791
3966
|
stepId: step.id,
|
|
2792
|
-
|
|
3967
|
+
attempt: outcome.attempt ?? 1,
|
|
3968
|
+
...outcome.executionIds && outcome.executionIds.length > 0 ? { executionIds: outcome.executionIds } : {},
|
|
3969
|
+
status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
|
|
2793
3970
|
...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
|
|
2794
3971
|
...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
|
|
2795
3972
|
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
@@ -2798,10 +3975,12 @@ ${contribution}`);
|
|
|
2798
3975
|
options.onStep?.(stepRun);
|
|
2799
3976
|
});
|
|
2800
3977
|
run2.reports = [...allReports];
|
|
2801
|
-
await this.
|
|
3978
|
+
await this.persist(run2);
|
|
2802
3979
|
}
|
|
2803
|
-
const
|
|
2804
|
-
const
|
|
3980
|
+
const allOutcomes = [...outcomes.values()];
|
|
3981
|
+
const failed = !cancelled && allOutcomes.some((o) => o.status === "failed");
|
|
3982
|
+
const partial = !cancelled && !failed && allOutcomes.some((o) => o.status === "partial");
|
|
3983
|
+
const finalStatus = cancelled ? "cancelled" : failed ? "failed" : partial ? "partial" : "completed";
|
|
2805
3984
|
const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
|
|
2806
3985
|
let resultSummary;
|
|
2807
3986
|
for (let i = def.steps.length - 1; i >= 0; i -= 1) {
|
|
@@ -2829,16 +4008,19 @@ ${contribution}`);
|
|
|
2829
4008
|
run2.reports = collected;
|
|
2830
4009
|
}
|
|
2831
4010
|
}
|
|
4011
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4012
|
+
this.activeRuns.delete(run2.id);
|
|
2832
4013
|
const finished = {
|
|
2833
4014
|
...run2,
|
|
2834
4015
|
status: finalStatus,
|
|
2835
|
-
finishedAt
|
|
4016
|
+
finishedAt,
|
|
4017
|
+
attempts: [...run2.attempts ?? [], { attempt: run2.attempt, startedAt: run2.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
|
|
2836
4018
|
...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
|
|
2837
4019
|
...combinedReportId !== void 0 ? { combinedReportId } : {},
|
|
2838
4020
|
...planId !== void 0 ? { planId } : {},
|
|
2839
4021
|
...resultSummary !== void 0 ? { resultSummary } : {}
|
|
2840
4022
|
};
|
|
2841
|
-
await this.
|
|
4023
|
+
await this.persist(finished);
|
|
2842
4024
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
2843
4025
|
await this.options.audit?.record({
|
|
2844
4026
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2848,6 +4030,23 @@ ${contribution}`);
|
|
|
2848
4030
|
});
|
|
2849
4031
|
return finished;
|
|
2850
4032
|
}
|
|
4033
|
+
/** First-class cancellation (§9.2): requests cancellation and terminates
|
|
4034
|
+
* tasks that are running in this process immediately. */
|
|
4035
|
+
async cancel(runId) {
|
|
4036
|
+
const run2 = await this.options.store.get(runId);
|
|
4037
|
+
if (!run2) {
|
|
4038
|
+
throw new PipelineError("PIPELINE_NOT_FOUND", `Pipeline run "${runId}" does not exist.`);
|
|
4039
|
+
}
|
|
4040
|
+
await this.options.store.save({
|
|
4041
|
+
...run2,
|
|
4042
|
+
cancellationRequested: true,
|
|
4043
|
+
cancellationRequestedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4044
|
+
});
|
|
4045
|
+
const active = this.activeRuns.get(runId);
|
|
4046
|
+
if (active) {
|
|
4047
|
+
await active.cancelTasks();
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
2851
4050
|
async get(runId) {
|
|
2852
4051
|
return this.options.store.get(runId);
|
|
2853
4052
|
}
|
|
@@ -2871,9 +4070,16 @@ ${contribution}`);
|
|
|
2871
4070
|
this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
|
|
2872
4071
|
}
|
|
2873
4072
|
last = await this.executeStepOnce(step, ctx);
|
|
2874
|
-
if (last.status !== "failed") return last;
|
|
4073
|
+
if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
|
|
2875
4074
|
}
|
|
2876
|
-
return last;
|
|
4075
|
+
return { ...last, attempt: retries + 1 };
|
|
4076
|
+
}
|
|
4077
|
+
/** Registers tasks created for a step as live, so cancellation can terminate them (§9.2). */
|
|
4078
|
+
trackLiveTasks(runId, taskIds) {
|
|
4079
|
+
if (!runId) return;
|
|
4080
|
+
const active = this.activeRuns.get(runId);
|
|
4081
|
+
if (!active) return;
|
|
4082
|
+
for (const id of taskIds) active.taskIds.add(id);
|
|
2877
4083
|
}
|
|
2878
4084
|
async executeStepOnce(step, ctx) {
|
|
2879
4085
|
switch (step.kind) {
|
|
@@ -2906,7 +4112,10 @@ ${contribution}`);
|
|
|
2906
4112
|
})
|
|
2907
4113
|
)
|
|
2908
4114
|
);
|
|
4115
|
+
this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
|
|
2909
4116
|
const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
|
|
4117
|
+
const executionIds = results.map((r) => r.executions?.at(-1)?.executionId).filter((id) => id !== void 0);
|
|
4118
|
+
const completedTasks = results.filter((r) => r.status === "completed");
|
|
2910
4119
|
const failedTasks = results.filter((r) => r.status !== "completed");
|
|
2911
4120
|
const lines = results.map((result, index) => {
|
|
2912
4121
|
const agentName = agentNames[index] ?? "unknown";
|
|
@@ -2916,16 +4125,25 @@ ${contribution}`);
|
|
|
2916
4125
|
const reports = results.flatMap((r) => r.result?.reports ?? []);
|
|
2917
4126
|
const summary = lines.join("\n");
|
|
2918
4127
|
if (failedTasks.length > 0) {
|
|
2919
|
-
const firstError = failedTasks[0]?.result?.error ?? "unknown
|
|
4128
|
+
const firstError = failedTasks[0]?.result?.error ?? failedTasks[0]?.lastError ?? "unknown outcome";
|
|
4129
|
+
const states = failedTasks.map((r) => r.status).join(", ");
|
|
2920
4130
|
return {
|
|
2921
|
-
|
|
4131
|
+
// Some agents succeeded → the step produced usable but incomplete
|
|
4132
|
+
// intelligence: partial, not failed (roadmap §9.3).
|
|
4133
|
+
status: completedTasks.length > 0 ? "partial" : "failed",
|
|
2922
4134
|
summary,
|
|
2923
4135
|
taskIds: created.map((t) => t.id),
|
|
2924
|
-
error: `${failedTasks.length}/${results.length} task(s)
|
|
4136
|
+
error: `${failedTasks.length}/${results.length} task(s) incomplete (${states}) \u2014 ${firstError}`,
|
|
2925
4137
|
reports
|
|
2926
4138
|
};
|
|
2927
4139
|
}
|
|
2928
|
-
return {
|
|
4140
|
+
return {
|
|
4141
|
+
status: "completed",
|
|
4142
|
+
summary,
|
|
4143
|
+
taskIds: created.map((t) => t.id),
|
|
4144
|
+
...executionIds.length > 0 ? { executionIds } : {},
|
|
4145
|
+
reports
|
|
4146
|
+
};
|
|
2929
4147
|
}
|
|
2930
4148
|
async runSingle(step, ctx, agentName) {
|
|
2931
4149
|
const task = await this.options.tasks.create({
|
|
@@ -2934,10 +4152,13 @@ ${contribution}`);
|
|
|
2934
4152
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
2935
4153
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
2936
4154
|
});
|
|
4155
|
+
this.trackLiveTasks(ctx.runId, [task.id]);
|
|
2937
4156
|
const result = await this.options.tasks.run(task.id);
|
|
2938
4157
|
if (result.status !== "completed") {
|
|
2939
4158
|
return {
|
|
2940
|
-
|
|
4159
|
+
// Unknown/timed_out outcomes are not failures — the work may have
|
|
4160
|
+
// happened remotely; the step is incomplete, not broken.
|
|
4161
|
+
status: result.status === "unknown" || result.status === "timed_out" ? "partial" : "failed",
|
|
2941
4162
|
summary: result.result?.summary ?? "",
|
|
2942
4163
|
taskIds: [task.id],
|
|
2943
4164
|
error: result.result?.error ?? `task ${task.id} did not complete`,
|
|
@@ -3053,114 +4274,44 @@ function createDefaultChatFn(providers) {
|
|
|
3053
4274
|
}
|
|
3054
4275
|
|
|
3055
4276
|
// src/pipelines/store.ts
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
filePath;
|
|
3060
|
-
queue = Promise.resolve();
|
|
3061
|
-
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
3062
|
-
this.filePath = `${directory}/pipelines.json`;
|
|
4277
|
+
var FilePipelineRunStore = class extends JsonFileStore {
|
|
4278
|
+
constructor(directory) {
|
|
4279
|
+
super({ ...directory !== void 0 ? { directory } : {}, fileName: "pipelines.json" });
|
|
3063
4280
|
}
|
|
3064
|
-
|
|
3065
|
-
return this.
|
|
3066
|
-
const runs = await this.load();
|
|
4281
|
+
save(run2) {
|
|
4282
|
+
return this.mutate((runs) => {
|
|
3067
4283
|
const index = runs.findIndex((r) => r.id === run2.id);
|
|
3068
4284
|
if (index >= 0) runs[index] = run2;
|
|
3069
4285
|
else runs.push(run2);
|
|
3070
|
-
|
|
4286
|
+
return [runs, void 0];
|
|
3071
4287
|
});
|
|
3072
4288
|
}
|
|
3073
4289
|
async get(id) {
|
|
3074
|
-
return this.
|
|
3075
|
-
const runs = await this.load();
|
|
3076
|
-
return runs.find((r) => r.id === id);
|
|
3077
|
-
});
|
|
4290
|
+
return (await this.readAll()).find((r) => r.id === id);
|
|
3078
4291
|
}
|
|
3079
4292
|
async list(filter = {}) {
|
|
3080
|
-
return this.
|
|
3081
|
-
const runs = await this.load();
|
|
3082
|
-
return runs.filter((run2) => filter.pipelineId ? run2.pipelineId === filter.pipelineId : true).filter((run2) => filter.status ? run2.status === filter.status : true).sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? ""));
|
|
3083
|
-
});
|
|
3084
|
-
}
|
|
3085
|
-
synchronized(operation) {
|
|
3086
|
-
const result = this.queue.then(operation, operation);
|
|
3087
|
-
this.queue = result.then(
|
|
3088
|
-
() => void 0,
|
|
3089
|
-
() => void 0
|
|
3090
|
-
);
|
|
3091
|
-
return result;
|
|
3092
|
-
}
|
|
3093
|
-
async load() {
|
|
3094
|
-
try {
|
|
3095
|
-
const raw = await readFile4(this.filePath, "utf8");
|
|
3096
|
-
const parsed = JSON.parse(raw);
|
|
3097
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
3098
|
-
} catch {
|
|
3099
|
-
return [];
|
|
3100
|
-
}
|
|
3101
|
-
}
|
|
3102
|
-
async write(runs) {
|
|
3103
|
-
const tmp = `${this.filePath}.tmp`;
|
|
3104
|
-
await mkdir5(dirname5(this.filePath), { recursive: true });
|
|
3105
|
-
await writeFile4(tmp, `${JSON.stringify(runs, null, 2)}
|
|
3106
|
-
`, "utf8");
|
|
3107
|
-
await rename4(tmp, this.filePath);
|
|
4293
|
+
return (await this.readAll()).filter((run2) => filter.pipelineId ? run2.pipelineId === filter.pipelineId : true).filter((run2) => filter.status ? run2.status === filter.status : true).sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? ""));
|
|
3108
4294
|
}
|
|
3109
4295
|
};
|
|
3110
4296
|
|
|
3111
4297
|
// src/planner/store.ts
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
filePath;
|
|
3116
|
-
queue = Promise.resolve();
|
|
3117
|
-
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
3118
|
-
this.filePath = `${directory}/plans.json`;
|
|
4298
|
+
var FilePlanStore = class extends JsonFileStore {
|
|
4299
|
+
constructor(directory) {
|
|
4300
|
+
super({ ...directory !== void 0 ? { directory } : {}, fileName: "plans.json" });
|
|
3119
4301
|
}
|
|
3120
|
-
|
|
3121
|
-
return this.
|
|
3122
|
-
const plans = await this.load();
|
|
4302
|
+
save(plan) {
|
|
4303
|
+
return this.mutate((plans) => {
|
|
3123
4304
|
const index = plans.findIndex((p) => p.id === plan.id);
|
|
3124
4305
|
if (index >= 0) plans[index] = plan;
|
|
3125
4306
|
else plans.push(plan);
|
|
3126
|
-
|
|
4307
|
+
return [plans, void 0];
|
|
3127
4308
|
});
|
|
3128
4309
|
}
|
|
3129
4310
|
async get(id) {
|
|
3130
|
-
return this.
|
|
3131
|
-
const plans = await this.load();
|
|
3132
|
-
return plans.find((p) => p.id === id);
|
|
3133
|
-
});
|
|
4311
|
+
return (await this.readAll()).find((p) => p.id === id);
|
|
3134
4312
|
}
|
|
3135
4313
|
async list(filter = {}) {
|
|
3136
|
-
return this.
|
|
3137
|
-
const plans = await this.load();
|
|
3138
|
-
return plans.filter((plan) => filter.pipelineRunId ? plan.pipelineRunId === filter.pipelineRunId : true).filter((plan) => filter.taskId ? plan.taskId === filter.taskId : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3139
|
-
});
|
|
3140
|
-
}
|
|
3141
|
-
synchronized(operation) {
|
|
3142
|
-
const result = this.queue.then(operation, operation);
|
|
3143
|
-
this.queue = result.then(
|
|
3144
|
-
() => void 0,
|
|
3145
|
-
() => void 0
|
|
3146
|
-
);
|
|
3147
|
-
return result;
|
|
3148
|
-
}
|
|
3149
|
-
async load() {
|
|
3150
|
-
try {
|
|
3151
|
-
const raw = await readFile5(this.filePath, "utf8");
|
|
3152
|
-
const parsed = JSON.parse(raw);
|
|
3153
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
3154
|
-
} catch {
|
|
3155
|
-
return [];
|
|
3156
|
-
}
|
|
3157
|
-
}
|
|
3158
|
-
async write(plans) {
|
|
3159
|
-
const tmp = `${this.filePath}.tmp`;
|
|
3160
|
-
await mkdir6(dirname6(this.filePath), { recursive: true });
|
|
3161
|
-
await writeFile5(tmp, `${JSON.stringify(plans, null, 2)}
|
|
3162
|
-
`, "utf8");
|
|
3163
|
-
await rename5(tmp, this.filePath);
|
|
4314
|
+
return (await this.readAll()).filter((plan) => filter.pipelineRunId ? plan.pipelineRunId === filter.pipelineRunId : true).filter((plan) => filter.taskId ? plan.taskId === filter.taskId : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3164
4315
|
}
|
|
3165
4316
|
};
|
|
3166
4317
|
|
|
@@ -3194,7 +4345,11 @@ function buildPipelineEngine(config, log = logger) {
|
|
|
3194
4345
|
agents,
|
|
3195
4346
|
roles,
|
|
3196
4347
|
store: new FileTaskStore(),
|
|
3197
|
-
...config.project.memory !== void 0 ? {
|
|
4348
|
+
...config.project.memory !== void 0 ? {
|
|
4349
|
+
memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
|
|
4350
|
+
required: config.project.memory.required
|
|
4351
|
+
})
|
|
4352
|
+
} : {},
|
|
3198
4353
|
audit: new FileAuditLog(),
|
|
3199
4354
|
log
|
|
3200
4355
|
});
|
|
@@ -3252,6 +4407,24 @@ function registerPipelineCommands(program) {
|
|
|
3252
4407
|
}
|
|
3253
4408
|
await runPipelineAction(def.id, objective ?? previous.objective);
|
|
3254
4409
|
});
|
|
4410
|
+
pipeline.command("cancel <run-id>").description("Request cancellation of a pipeline run (stops scheduling and cancels running tasks).").action(async (runId) => {
|
|
4411
|
+
const config = loadConfig();
|
|
4412
|
+
const engine = buildPipelineEngine(config);
|
|
4413
|
+
await engine.cancel(runId);
|
|
4414
|
+
console.log(`Cancellation requested for run ${runId}.`);
|
|
4415
|
+
console.log("A run executing in another process stops at its next check point.");
|
|
4416
|
+
});
|
|
4417
|
+
pipeline.command("validate <id>").description("Validate a pipeline definition before running it (\xA79.5).").action(async (id) => {
|
|
4418
|
+
const config = loadConfig();
|
|
4419
|
+
const def = findPipeline(config, id);
|
|
4420
|
+
const engine = buildPipelineEngine(config);
|
|
4421
|
+
engine.validate(def);
|
|
4422
|
+
console.log(`Pipeline "${def.id}" is valid (${def.steps.length} step(s)).`);
|
|
4423
|
+
for (const step of def.steps) {
|
|
4424
|
+
const deps = step.dependsOn?.join(", ") ?? "(previous step)";
|
|
4425
|
+
console.log(` ${step.id} kind=${step.kind} depends_on=${deps}`);
|
|
4426
|
+
}
|
|
4427
|
+
});
|
|
3255
4428
|
pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
|
|
3256
4429
|
const config = loadConfig();
|
|
3257
4430
|
const engine = buildPipelineEngine(config);
|
|
@@ -3261,7 +4434,8 @@ function registerPipelineCommands(program) {
|
|
|
3261
4434
|
return;
|
|
3262
4435
|
}
|
|
3263
4436
|
for (const run2 of runs) {
|
|
3264
|
-
|
|
4437
|
+
const note = isInterrupted(run2) ? " (interrupted \u2014 process gone before finish)" : "";
|
|
4438
|
+
console.log(`${run2.id} ${run2.status.padEnd(10)} ${run2.startedAt ?? "?"}${note}`);
|
|
3265
4439
|
}
|
|
3266
4440
|
});
|
|
3267
4441
|
pipeline.command("status <run-id>").description("Show the recorded state of a pipeline run.").action(async (runId) => {
|
|
@@ -3279,6 +4453,13 @@ function registerPipelineCommands(program) {
|
|
|
3279
4453
|
console.log(` status: ${run2.status}`);
|
|
3280
4454
|
console.log(` started: ${run2.startedAt ?? "?"}`);
|
|
3281
4455
|
if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
|
|
4456
|
+
console.log(` attempt: ${run2.attempt}`);
|
|
4457
|
+
if (run2.cancellationRequested) {
|
|
4458
|
+
console.log(` cancellation requested at ${run2.cancellationRequestedAt ?? "?"}`);
|
|
4459
|
+
}
|
|
4460
|
+
if (isInterrupted(run2)) {
|
|
4461
|
+
console.log(" note: this run looks interrupted (no finishedAt) \u2014 retry with `pipeline retry`");
|
|
4462
|
+
}
|
|
3282
4463
|
if (run2.resultSummary) console.log(` result: ${run2.resultSummary}`);
|
|
3283
4464
|
for (const stepRun of run2.stepRuns) {
|
|
3284
4465
|
console.log(` [${stepRun.stepId}] ${stepRun.status}`);
|
|
@@ -3320,6 +4501,9 @@ async function runPipelineAction(id, objective) {
|
|
|
3320
4501
|
function registerRunAlias(program) {
|
|
3321
4502
|
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));
|
|
3322
4503
|
}
|
|
4504
|
+
function isInterrupted(run2) {
|
|
4505
|
+
return run2.status === "running" && run2.finishedAt === void 0;
|
|
4506
|
+
}
|
|
3323
4507
|
function findPipeline(config, id) {
|
|
3324
4508
|
const def = config.project.pipelines.find((p) => p.id === id);
|
|
3325
4509
|
if (!def) {
|