@hilbras/omninode 2.0.0-alpha.1 → 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 +66 -10
- package/dist/cli/index.js +1580 -280
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +896 -105
- package/dist/index.js +1575 -255
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/version.ts
|
|
9
|
-
var OMNINODE_VERSION = "2.0.0-alpha.
|
|
9
|
+
var OMNINODE_VERSION = "2.0.0-alpha.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("; ");
|
|
@@ -1397,12 +2199,18 @@ import { randomBytes } from "crypto";
|
|
|
1397
2199
|
var ALLOWED_TRANSITIONS = {
|
|
1398
2200
|
created: ["queued", "running", "cancelled"],
|
|
1399
2201
|
queued: ["running", "cancelled"],
|
|
1400
|
-
running: ["completed", "failed", "cancelled"],
|
|
2202
|
+
running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
|
|
1401
2203
|
waiting: ["running", "cancelled"],
|
|
1402
2204
|
completed: [],
|
|
1403
|
-
failed:
|
|
1404
|
-
|
|
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: []
|
|
1405
2211
|
};
|
|
2212
|
+
var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
|
|
2213
|
+
var EXECUTION_HISTORY_LIMIT = 10;
|
|
1406
2214
|
var TaskEngine = class {
|
|
1407
2215
|
constructor(options) {
|
|
1408
2216
|
this.options = options;
|
|
@@ -1434,6 +2242,7 @@ var TaskEngine = class {
|
|
|
1434
2242
|
id: generateTaskId(),
|
|
1435
2243
|
objective: input.objective.trim(),
|
|
1436
2244
|
status: "created",
|
|
2245
|
+
attempt: 0,
|
|
1437
2246
|
createdAt: now,
|
|
1438
2247
|
updatedAt: now,
|
|
1439
2248
|
...input.project !== void 0 ? { project: input.project } : {},
|
|
@@ -1450,8 +2259,10 @@ var TaskEngine = class {
|
|
|
1450
2259
|
const task = await this.mustGet(id);
|
|
1451
2260
|
return this.transition(task, "queued");
|
|
1452
2261
|
}
|
|
1453
|
-
/** Runs the task through its assigned agent and records the result.
|
|
1454
|
-
|
|
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 = {}) {
|
|
1455
2266
|
const task = await this.mustGet(id);
|
|
1456
2267
|
if (task.status !== "created" && task.status !== "queued") {
|
|
1457
2268
|
throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
|
|
@@ -1464,49 +2275,89 @@ var TaskEngine = class {
|
|
|
1464
2275
|
throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
|
|
1465
2276
|
}
|
|
1466
2277
|
const role = task.role ? this.options.roles.get(task.role) : void 0;
|
|
1467
|
-
const running = await this.transition(task, "running");
|
|
1468
2278
|
this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
|
|
1469
|
-
let background =
|
|
2279
|
+
let background = task.context?.background;
|
|
1470
2280
|
if (this.options.memory) {
|
|
1471
2281
|
const memoryContext = await this.options.memory.gatherContext({
|
|
1472
|
-
objective:
|
|
1473
|
-
...
|
|
1474
|
-
...
|
|
2282
|
+
objective: task.objective,
|
|
2283
|
+
...task.role ? { role: task.role } : {},
|
|
2284
|
+
...task.project ? { project: task.project } : {}
|
|
1475
2285
|
});
|
|
1476
2286
|
if (memoryContext.length > 0) {
|
|
1477
2287
|
background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
|
|
1478
2288
|
}
|
|
1479
2289
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
});
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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 }
|
|
1493
2353
|
});
|
|
2354
|
+
this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
|
|
2355
|
+
break;
|
|
1494
2356
|
}
|
|
1495
|
-
const result = {
|
|
1496
|
-
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
1497
|
-
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
1498
|
-
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
1499
|
-
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
1500
|
-
...output.error !== void 0 ? { error: output.error } : {},
|
|
1501
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1502
|
-
};
|
|
1503
|
-
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
1504
|
-
const finished = await this.transition(running, finalStatus, { result });
|
|
1505
|
-
this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
|
|
1506
2357
|
if (this.options.memory) {
|
|
1507
|
-
await this.options.memory.recordTaskOutcome(
|
|
2358
|
+
await this.options.memory.recordTaskOutcome(current);
|
|
1508
2359
|
}
|
|
1509
|
-
return
|
|
2360
|
+
return current;
|
|
1510
2361
|
}
|
|
1511
2362
|
async cancel(id) {
|
|
1512
2363
|
const task = await this.mustGet(id);
|
|
@@ -1519,13 +2370,14 @@ var TaskEngine = class {
|
|
|
1519
2370
|
}
|
|
1520
2371
|
return this.transition(task, "cancelled");
|
|
1521
2372
|
}
|
|
1522
|
-
/** Error recovery
|
|
2373
|
+
/** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
|
|
1523
2374
|
async retry(id) {
|
|
1524
2375
|
const task = await this.mustGet(id);
|
|
1525
|
-
|
|
2376
|
+
const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
|
|
2377
|
+
if (!retryableStates.includes(task.status)) {
|
|
1526
2378
|
throw new TaskError(
|
|
1527
2379
|
"TASK_INVALID",
|
|
1528
|
-
`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}".`
|
|
1529
2381
|
);
|
|
1530
2382
|
}
|
|
1531
2383
|
const { result: _discarded, ...rest } = task;
|
|
@@ -1557,10 +2409,14 @@ var TaskEngine = class {
|
|
|
1557
2409
|
if (!allowed.includes(to)) {
|
|
1558
2410
|
throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
|
|
1559
2411
|
}
|
|
2412
|
+
const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
|
|
1560
2413
|
const updated = {
|
|
1561
2414
|
...task,
|
|
1562
2415
|
status: to,
|
|
1563
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 } : {},
|
|
1564
2420
|
...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
|
|
1565
2421
|
};
|
|
1566
2422
|
await this.options.store.save(updated);
|
|
@@ -1581,6 +2437,19 @@ var TaskEngine = class {
|
|
|
1581
2437
|
function generateTaskId() {
|
|
1582
2438
|
return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
1583
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
|
+
}
|
|
1584
2453
|
|
|
1585
2454
|
// src/audit/index.ts
|
|
1586
2455
|
import { appendFile, mkdir as mkdir2 } from "fs/promises";
|
|
@@ -1623,6 +2492,23 @@ var FileAuditLog = class {
|
|
|
1623
2492
|
}
|
|
1624
2493
|
};
|
|
1625
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
|
+
|
|
1626
2512
|
// src/memory/local.ts
|
|
1627
2513
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1628
2514
|
"the",
|
|
@@ -1685,9 +2571,27 @@ var LocalMemoryProvider = class extends JsonFileStore {
|
|
|
1685
2571
|
constructor(directory) {
|
|
1686
2572
|
super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
|
|
1687
2573
|
}
|
|
1688
|
-
|
|
2574
|
+
metadata() {
|
|
2575
|
+
return defaultMetadata(this.name, {
|
|
2576
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
2577
|
+
relevanceRanking: true,
|
|
2578
|
+
backend: { kind: "json-file", deterministicScoring: true }
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
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) {
|
|
1689
2593
|
const entries = await this.readAll();
|
|
1690
|
-
const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter(
|
|
2594
|
+
const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter((entry2) => query.category ? entry2.category === query.category : true).filter(
|
|
1691
2595
|
(entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
|
|
1692
2596
|
);
|
|
1693
2597
|
if (!query.text || query.text.trim().length === 0) {
|
|
@@ -1696,7 +2600,7 @@ var LocalMemoryProvider = class extends JsonFileStore {
|
|
|
1696
2600
|
const queryKeywords = keywords(query.text);
|
|
1697
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);
|
|
1698
2602
|
}
|
|
1699
|
-
|
|
2603
|
+
store(entry2) {
|
|
1700
2604
|
return this.mutate((entries) => {
|
|
1701
2605
|
const index = entries.findIndex((e) => e.key === entry2.key);
|
|
1702
2606
|
const stamped = {
|
|
@@ -1727,11 +2631,40 @@ var RememberaMemoryProvider = class {
|
|
|
1727
2631
|
this.env = config.env ?? process.env;
|
|
1728
2632
|
this.log = log.child({ component: "remembera" });
|
|
1729
2633
|
}
|
|
2634
|
+
get authenticationConfigured() {
|
|
2635
|
+
return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
|
|
2636
|
+
}
|
|
1730
2637
|
headers() {
|
|
1731
2638
|
const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
|
|
1732
2639
|
return key ? { authorization: `Bearer ${key}` } : {};
|
|
1733
2640
|
}
|
|
1734
|
-
|
|
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) {
|
|
1735
2668
|
const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
|
|
1736
2669
|
if (status !== 200) {
|
|
1737
2670
|
throw new MemoryError(`Remembera query returned HTTP ${status}.`);
|
|
@@ -1742,7 +2675,7 @@ var RememberaMemoryProvider = class {
|
|
|
1742
2675
|
}
|
|
1743
2676
|
return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
|
|
1744
2677
|
}
|
|
1745
|
-
async
|
|
2678
|
+
async store(entry2) {
|
|
1746
2679
|
const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
|
|
1747
2680
|
if (status !== 200 && status !== 201) {
|
|
1748
2681
|
throw new MemoryError(`Remembera write returned HTTP ${status}.`);
|
|
@@ -1777,6 +2710,7 @@ var RememberaMemoryProvider = class {
|
|
|
1777
2710
|
content: entry2.content,
|
|
1778
2711
|
scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
|
|
1779
2712
|
...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
|
|
2713
|
+
...typeof entry2.category === "string" ? { category: entry2.category } : {},
|
|
1780
2714
|
...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
|
|
1781
2715
|
...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
|
|
1782
2716
|
};
|
|
@@ -1807,57 +2741,54 @@ function createMemoryProvider(config) {
|
|
|
1807
2741
|
}
|
|
1808
2742
|
|
|
1809
2743
|
// src/memory/service.ts
|
|
2744
|
+
var DEFAULT_ENTRY_LIMIT = 8;
|
|
1810
2745
|
var MAX_CONTEXT_CHARS = 4e3;
|
|
1811
2746
|
var MemoryService = class {
|
|
1812
|
-
constructor(provider, log = logger) {
|
|
2747
|
+
constructor(provider, log = logger, options = {}) {
|
|
1813
2748
|
this.provider = provider;
|
|
1814
2749
|
this.log = log.child({ component: "memory", provider: provider.name });
|
|
2750
|
+
this.required = options.required ?? false;
|
|
2751
|
+
this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
|
|
1815
2752
|
}
|
|
1816
2753
|
provider;
|
|
1817
2754
|
log;
|
|
2755
|
+
required;
|
|
2756
|
+
maxContextChars;
|
|
1818
2757
|
get providerRef() {
|
|
1819
2758
|
return this.provider;
|
|
1820
2759
|
}
|
|
1821
2760
|
get providerName() {
|
|
1822
2761
|
return this.provider.name;
|
|
1823
2762
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
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;
|
|
1826
2769
|
try {
|
|
1827
|
-
|
|
2770
|
+
entries = await this.provider.retrieve({
|
|
1828
2771
|
text: query.objective,
|
|
1829
2772
|
...query.role ? { tags: [query.role] } : {},
|
|
1830
|
-
limit:
|
|
2773
|
+
limit: query.limit ?? DEFAULT_ENTRY_LIMIT
|
|
1831
2774
|
});
|
|
1832
|
-
if (entries.length === 0) return "";
|
|
1833
|
-
const byScope = /* @__PURE__ */ new Map();
|
|
1834
|
-
for (const entry2 of entries) {
|
|
1835
|
-
byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
|
|
1836
|
-
}
|
|
1837
|
-
const parts = [];
|
|
1838
|
-
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
1839
|
-
const scopeEntries = byScope.get(scope) ?? [];
|
|
1840
|
-
if (scopeEntries.length === 0) continue;
|
|
1841
|
-
const lines = scopeEntries.map((entry2) => `- ${entry2.content}`).join("\n");
|
|
1842
|
-
parts.push(`### Relevant memory (${scope})
|
|
1843
|
-
${lines}`);
|
|
1844
|
-
}
|
|
1845
|
-
const context = parts.join("\n\n");
|
|
1846
|
-
return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
|
|
1847
2775
|
} catch (error) {
|
|
1848
|
-
this.
|
|
1849
|
-
`Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
|
|
1850
|
-
);
|
|
1851
|
-
return "";
|
|
2776
|
+
return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
|
|
1852
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;
|
|
1853
2783
|
}
|
|
1854
|
-
/** Records a finished task's outcome —
|
|
2784
|
+
/** Records a finished task's outcome — task history plus "known problems" (§12). */
|
|
1855
2785
|
async recordTaskOutcome(task) {
|
|
1856
2786
|
const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
|
|
1857
2787
|
try {
|
|
1858
|
-
await this.provider.
|
|
2788
|
+
await this.provider.store({
|
|
1859
2789
|
key: `task:${task.id}`,
|
|
1860
2790
|
scope: "task",
|
|
2791
|
+
category: "tasks",
|
|
1861
2792
|
content: `Objective: ${task.objective}
|
|
1862
2793
|
Status: ${task.status}
|
|
1863
2794
|
Outcome: ${outcome}`,
|
|
@@ -1868,18 +2799,53 @@ Outcome: ${outcome}`,
|
|
|
1868
2799
|
(report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
|
|
1869
2800
|
);
|
|
1870
2801
|
for (const { report, finding } of significant) {
|
|
1871
|
-
await this.provider.
|
|
2802
|
+
await this.provider.store({
|
|
1872
2803
|
key: `finding:${report.id}:${finding.id}`,
|
|
1873
2804
|
scope: "project",
|
|
2805
|
+
category: "problems",
|
|
1874
2806
|
content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
|
|
1875
2807
|
tags: ["known-problem", report.agent]
|
|
1876
2808
|
});
|
|
1877
2809
|
}
|
|
1878
2810
|
} catch (error) {
|
|
1879
|
-
this.
|
|
1880
|
-
|
|
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 }
|
|
1881
2845
|
);
|
|
1882
2846
|
}
|
|
2847
|
+
this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
|
|
2848
|
+
return fallback ?? { text: "", entries: [], truncated: false };
|
|
1883
2849
|
}
|
|
1884
2850
|
};
|
|
1885
2851
|
|
|
@@ -1913,7 +2879,11 @@ function createTaskEngine(config, log = logger, directory) {
|
|
|
1913
2879
|
agents,
|
|
1914
2880
|
roles,
|
|
1915
2881
|
store: new FileTaskStore(directory),
|
|
1916
|
-
...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
|
+
} : {},
|
|
1917
2887
|
audit: new FileAuditLog(),
|
|
1918
2888
|
log
|
|
1919
2889
|
});
|
|
@@ -1980,7 +2950,7 @@ function registerTaskCommands(program) {
|
|
|
1980
2950
|
const cancelled = await engine.cancel(taskId);
|
|
1981
2951
|
console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
|
|
1982
2952
|
});
|
|
1983
|
-
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) => {
|
|
1984
2954
|
const config = loadConfig();
|
|
1985
2955
|
const engine = createTaskEngine(config);
|
|
1986
2956
|
const reset = await engine.retry(taskId);
|
|
@@ -2003,6 +2973,15 @@ function printOutcome(task, options = {}) {
|
|
|
2003
2973
|
if (task.context?.files) console.log(` files: ${task.context.files.join(", ")}`);
|
|
2004
2974
|
console.log(` objective: ${task.objective}`);
|
|
2005
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
|
+
}
|
|
2006
2985
|
const result = task.result;
|
|
2007
2986
|
if (!result) return;
|
|
2008
2987
|
if (result.summary) console.log(` summary: ${result.summary}`);
|
|
@@ -2018,6 +2997,87 @@ function truncate2(text, max) {
|
|
|
2018
2997
|
|
|
2019
2998
|
// src/reports/aggregate.ts
|
|
2020
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
|
|
2021
3081
|
var SEVERITY_RANK = {
|
|
2022
3082
|
info: 0,
|
|
2023
3083
|
low: 1,
|
|
@@ -2039,7 +3099,12 @@ function aggregateReports(reports, options = {}) {
|
|
|
2039
3099
|
}
|
|
2040
3100
|
const sources = [...new Set(reports.map((r) => r.agent))].sort();
|
|
2041
3101
|
const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
|
|
2042
|
-
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).`;
|
|
2043
3108
|
return {
|
|
2044
3109
|
id: `combined-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
|
|
2045
3110
|
title: options.objective !== void 0 ? truncate3(options.objective, 120) : "Combined report",
|
|
@@ -2048,10 +3113,57 @@ function aggregateReports(reports, options = {}) {
|
|
|
2048
3113
|
taskIds: options.taskIds ?? [],
|
|
2049
3114
|
summary,
|
|
2050
3115
|
findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
|
|
3116
|
+
agreements,
|
|
3117
|
+
conflicts,
|
|
2051
3118
|
recommendations: recommendationGroups,
|
|
2052
3119
|
sources,
|
|
2053
3120
|
reportIds: reports.map((r) => r.id),
|
|
2054
|
-
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] } : {}
|
|
2055
3167
|
};
|
|
2056
3168
|
}
|
|
2057
3169
|
function rank(finding) {
|
|
@@ -2064,10 +3176,22 @@ function mergeFinding(groups, finding, report, threshold) {
|
|
|
2064
3176
|
group.occurrences += 1;
|
|
2065
3177
|
group.details.push(finding.detail);
|
|
2066
3178
|
group.sources.push(source);
|
|
2067
|
-
group.evidence.push(...finding.evidence ?? []);
|
|
3179
|
+
group.evidence.push(...normalizeEvidence(finding.evidence) ?? []);
|
|
2068
3180
|
if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
|
|
2069
3181
|
group.severity = finding.severity;
|
|
2070
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
|
+
}
|
|
2071
3195
|
return;
|
|
2072
3196
|
}
|
|
2073
3197
|
groups.push({
|
|
@@ -2076,7 +3200,11 @@ function mergeFinding(groups, finding, report, threshold) {
|
|
|
2076
3200
|
sources: [source],
|
|
2077
3201
|
occurrences: 1,
|
|
2078
3202
|
...finding.severity ? { severity: finding.severity } : {},
|
|
2079
|
-
|
|
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) ?? []]
|
|
2080
3208
|
});
|
|
2081
3209
|
}
|
|
2082
3210
|
function mergeRecommendation(groups, text, report, threshold) {
|
|
@@ -2158,6 +3286,7 @@ function extractReportFromText(raw, meta) {
|
|
|
2158
3286
|
id: `${meta.taskId}-f-${index + 1}`,
|
|
2159
3287
|
title: truncate4(text, 120),
|
|
2160
3288
|
detail: text,
|
|
3289
|
+
description: text,
|
|
2161
3290
|
severity: guessSeverity(text)
|
|
2162
3291
|
}));
|
|
2163
3292
|
return {
|
|
@@ -2168,7 +3297,8 @@ function extractReportFromText(raw, meta) {
|
|
|
2168
3297
|
summary,
|
|
2169
3298
|
findings,
|
|
2170
3299
|
recommendations: recommendationTexts.map((t) => truncate4(t, 200)),
|
|
2171
|
-
|
|
3300
|
+
...evidenceTexts.length > 0 ? { evidence: evidenceTexts.map((excerpt) => ({ kind: "observation", excerpt })) } : {},
|
|
3301
|
+
metadata: evidenceTexts.length > 0 ? { evidenceCount: evidenceTexts.length } : void 0,
|
|
2172
3302
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2173
3303
|
};
|
|
2174
3304
|
}
|
|
@@ -2184,27 +3314,46 @@ var ReportService = class {
|
|
|
2184
3314
|
}
|
|
2185
3315
|
store;
|
|
2186
3316
|
log;
|
|
3317
|
+
/** Rejected reports from the last collection (§13 — malformed never enters aggregation). */
|
|
3318
|
+
lastDiagnostics = { collected: 0, accepted: 0, rejected: [] };
|
|
2187
3319
|
get storeRef() {
|
|
2188
3320
|
return this.store;
|
|
2189
3321
|
}
|
|
2190
|
-
/** 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). */
|
|
2191
3324
|
async collectFromTasks(tasks) {
|
|
2192
|
-
const
|
|
3325
|
+
const candidates = [];
|
|
2193
3326
|
for (const task of tasks) {
|
|
2194
3327
|
const structured = task.result?.reports ?? [];
|
|
2195
3328
|
if (structured.length > 0) {
|
|
2196
|
-
|
|
3329
|
+
candidates.push(...structured);
|
|
2197
3330
|
continue;
|
|
2198
3331
|
}
|
|
2199
3332
|
const text = task.result?.rawOutput ?? task.result?.summary ?? "";
|
|
2200
3333
|
if (text.trim().length === 0) continue;
|
|
2201
|
-
|
|
3334
|
+
candidates.push(
|
|
2202
3335
|
extractReportFromText(text, {
|
|
2203
3336
|
taskId: task.id,
|
|
2204
3337
|
agent: task.agent ?? "unknown"
|
|
2205
3338
|
})
|
|
2206
3339
|
);
|
|
2207
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 };
|
|
2208
3357
|
return reports;
|
|
2209
3358
|
}
|
|
2210
3359
|
async saveReports(reports) {
|
|
@@ -2293,8 +3442,9 @@ function registerReportCommands(program) {
|
|
|
2293
3442
|
return;
|
|
2294
3443
|
}
|
|
2295
3444
|
for (const c of combined) {
|
|
3445
|
+
const conflicts = c.conflicts.length > 0 ? ` ${c.conflicts.length} CONFLICT(S)` : "";
|
|
2296
3446
|
console.log(
|
|
2297
|
-
`${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}` : ""}`
|
|
2298
3448
|
);
|
|
2299
3449
|
}
|
|
2300
3450
|
});
|
|
@@ -2331,6 +3481,21 @@ function registerReportCommands(program) {
|
|
|
2331
3481
|
console.log(` reports: ${combined.reportIds.length}`);
|
|
2332
3482
|
if (combined.pipelineRunId) console.log(` run: ${combined.pipelineRunId}`);
|
|
2333
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
|
+
}
|
|
2334
3499
|
if (combined.findings.length > 0) {
|
|
2335
3500
|
console.log(" findings (grouped, duplicates merged):");
|
|
2336
3501
|
for (const finding of combined.findings) {
|
|
@@ -2424,7 +3589,7 @@ function truncate5(text, max) {
|
|
|
2424
3589
|
|
|
2425
3590
|
// src/planner/model.ts
|
|
2426
3591
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
2427
|
-
import { z as
|
|
3592
|
+
import { z as z3 } from "zod";
|
|
2428
3593
|
|
|
2429
3594
|
// src/planner/context.ts
|
|
2430
3595
|
function buildPlannerContext(request) {
|
|
@@ -2477,21 +3642,21 @@ ${request.context}`);
|
|
|
2477
3642
|
}
|
|
2478
3643
|
|
|
2479
3644
|
// src/planner/model.ts
|
|
2480
|
-
var planStepJsonSchema =
|
|
2481
|
-
id:
|
|
2482
|
-
title:
|
|
2483
|
-
description:
|
|
2484
|
-
order:
|
|
2485
|
-
targets:
|
|
2486
|
-
acceptance_criteria:
|
|
2487
|
-
depends_on:
|
|
2488
|
-
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()
|
|
2489
3654
|
}).strict();
|
|
2490
|
-
var planJsonSchema =
|
|
2491
|
-
summary:
|
|
2492
|
-
steps:
|
|
2493
|
-
risks:
|
|
2494
|
-
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()
|
|
2495
3660
|
}).strict();
|
|
2496
3661
|
var ModelPlanner = class {
|
|
2497
3662
|
constructor(options) {
|
|
@@ -2584,9 +3749,23 @@ var PipelineEngine = class {
|
|
|
2584
3749
|
}
|
|
2585
3750
|
options;
|
|
2586
3751
|
log;
|
|
3752
|
+
/** Runs executing in THIS process — cancellation can terminate them directly. */
|
|
3753
|
+
activeRuns = /* @__PURE__ */ new Map();
|
|
2587
3754
|
get store() {
|
|
2588
3755
|
return this.options.store;
|
|
2589
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
|
+
}
|
|
2590
3769
|
/** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
|
|
2591
3770
|
validate(def) {
|
|
2592
3771
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -2686,18 +3865,44 @@ var PipelineEngine = class {
|
|
|
2686
3865
|
);
|
|
2687
3866
|
}
|
|
2688
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
|
+
}
|
|
2689
3884
|
}
|
|
2690
3885
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2691
3886
|
const run2 = {
|
|
2692
3887
|
id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
|
|
2693
3888
|
pipelineId: def.id,
|
|
3889
|
+
attempt: 1,
|
|
2694
3890
|
objective,
|
|
2695
3891
|
status: "running",
|
|
2696
3892
|
startedAt: now,
|
|
2697
3893
|
stepRuns: []
|
|
2698
3894
|
};
|
|
2699
3895
|
await this.options.store.save(run2);
|
|
2700
|
-
|
|
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}).`);
|
|
2701
3906
|
await this.options.audit?.record({
|
|
2702
3907
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2703
3908
|
action: "pipeline.run.started",
|
|
@@ -2709,7 +3914,22 @@ var PipelineEngine = class {
|
|
|
2709
3914
|
const pending = new Set(def.steps.map((s) => s.id));
|
|
2710
3915
|
const contextParts = [];
|
|
2711
3916
|
const allReports = [];
|
|
3917
|
+
let cancelled = false;
|
|
2712
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
|
+
}
|
|
2713
3933
|
const ready = def.steps.filter(
|
|
2714
3934
|
(step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
|
|
2715
3935
|
);
|
|
@@ -2726,7 +3946,8 @@ var PipelineEngine = class {
|
|
|
2726
3946
|
...role ? { role } : {},
|
|
2727
3947
|
combinedContext: contextParts.join("\n\n"),
|
|
2728
3948
|
reports: [...allReports],
|
|
2729
|
-
pipelineRunId: run2.id
|
|
3949
|
+
pipelineRunId: run2.id,
|
|
3950
|
+
runId: run2.id
|
|
2730
3951
|
};
|
|
2731
3952
|
return this.executeStep(step, ctx);
|
|
2732
3953
|
})
|
|
@@ -2743,7 +3964,9 @@ ${contribution}`);
|
|
|
2743
3964
|
}
|
|
2744
3965
|
const stepRun = {
|
|
2745
3966
|
stepId: step.id,
|
|
2746
|
-
|
|
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",
|
|
2747
3970
|
...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
|
|
2748
3971
|
...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
|
|
2749
3972
|
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
@@ -2752,10 +3975,12 @@ ${contribution}`);
|
|
|
2752
3975
|
options.onStep?.(stepRun);
|
|
2753
3976
|
});
|
|
2754
3977
|
run2.reports = [...allReports];
|
|
2755
|
-
await this.
|
|
3978
|
+
await this.persist(run2);
|
|
2756
3979
|
}
|
|
2757
|
-
const
|
|
2758
|
-
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";
|
|
2759
3984
|
const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
|
|
2760
3985
|
let resultSummary;
|
|
2761
3986
|
for (let i = def.steps.length - 1; i >= 0; i -= 1) {
|
|
@@ -2783,16 +4008,19 @@ ${contribution}`);
|
|
|
2783
4008
|
run2.reports = collected;
|
|
2784
4009
|
}
|
|
2785
4010
|
}
|
|
4011
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4012
|
+
this.activeRuns.delete(run2.id);
|
|
2786
4013
|
const finished = {
|
|
2787
4014
|
...run2,
|
|
2788
4015
|
status: finalStatus,
|
|
2789
|
-
finishedAt
|
|
4016
|
+
finishedAt,
|
|
4017
|
+
attempts: [...run2.attempts ?? [], { attempt: run2.attempt, startedAt: run2.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
|
|
2790
4018
|
...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
|
|
2791
4019
|
...combinedReportId !== void 0 ? { combinedReportId } : {},
|
|
2792
4020
|
...planId !== void 0 ? { planId } : {},
|
|
2793
4021
|
...resultSummary !== void 0 ? { resultSummary } : {}
|
|
2794
4022
|
};
|
|
2795
|
-
await this.
|
|
4023
|
+
await this.persist(finished);
|
|
2796
4024
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
2797
4025
|
await this.options.audit?.record({
|
|
2798
4026
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2802,6 +4030,23 @@ ${contribution}`);
|
|
|
2802
4030
|
});
|
|
2803
4031
|
return finished;
|
|
2804
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
|
+
}
|
|
2805
4050
|
async get(runId) {
|
|
2806
4051
|
return this.options.store.get(runId);
|
|
2807
4052
|
}
|
|
@@ -2825,9 +4070,16 @@ ${contribution}`);
|
|
|
2825
4070
|
this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
|
|
2826
4071
|
}
|
|
2827
4072
|
last = await this.executeStepOnce(step, ctx);
|
|
2828
|
-
if (last.status !== "failed") return last;
|
|
4073
|
+
if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
|
|
2829
4074
|
}
|
|
2830
|
-
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);
|
|
2831
4083
|
}
|
|
2832
4084
|
async executeStepOnce(step, ctx) {
|
|
2833
4085
|
switch (step.kind) {
|
|
@@ -2860,7 +4112,10 @@ ${contribution}`);
|
|
|
2860
4112
|
})
|
|
2861
4113
|
)
|
|
2862
4114
|
);
|
|
4115
|
+
this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
|
|
2863
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");
|
|
2864
4119
|
const failedTasks = results.filter((r) => r.status !== "completed");
|
|
2865
4120
|
const lines = results.map((result, index) => {
|
|
2866
4121
|
const agentName = agentNames[index] ?? "unknown";
|
|
@@ -2870,16 +4125,25 @@ ${contribution}`);
|
|
|
2870
4125
|
const reports = results.flatMap((r) => r.result?.reports ?? []);
|
|
2871
4126
|
const summary = lines.join("\n");
|
|
2872
4127
|
if (failedTasks.length > 0) {
|
|
2873
|
-
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(", ");
|
|
2874
4130
|
return {
|
|
2875
|
-
|
|
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",
|
|
2876
4134
|
summary,
|
|
2877
4135
|
taskIds: created.map((t) => t.id),
|
|
2878
|
-
error: `${failedTasks.length}/${results.length} task(s)
|
|
4136
|
+
error: `${failedTasks.length}/${results.length} task(s) incomplete (${states}) \u2014 ${firstError}`,
|
|
2879
4137
|
reports
|
|
2880
4138
|
};
|
|
2881
4139
|
}
|
|
2882
|
-
return {
|
|
4140
|
+
return {
|
|
4141
|
+
status: "completed",
|
|
4142
|
+
summary,
|
|
4143
|
+
taskIds: created.map((t) => t.id),
|
|
4144
|
+
...executionIds.length > 0 ? { executionIds } : {},
|
|
4145
|
+
reports
|
|
4146
|
+
};
|
|
2883
4147
|
}
|
|
2884
4148
|
async runSingle(step, ctx, agentName) {
|
|
2885
4149
|
const task = await this.options.tasks.create({
|
|
@@ -2888,10 +4152,13 @@ ${contribution}`);
|
|
|
2888
4152
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
2889
4153
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
2890
4154
|
});
|
|
4155
|
+
this.trackLiveTasks(ctx.runId, [task.id]);
|
|
2891
4156
|
const result = await this.options.tasks.run(task.id);
|
|
2892
4157
|
if (result.status !== "completed") {
|
|
2893
4158
|
return {
|
|
2894
|
-
|
|
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",
|
|
2895
4162
|
summary: result.result?.summary ?? "",
|
|
2896
4163
|
taskIds: [task.id],
|
|
2897
4164
|
error: result.result?.error ?? `task ${task.id} did not complete`,
|
|
@@ -3078,7 +4345,11 @@ function buildPipelineEngine(config, log = logger) {
|
|
|
3078
4345
|
agents,
|
|
3079
4346
|
roles,
|
|
3080
4347
|
store: new FileTaskStore(),
|
|
3081
|
-
...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
|
+
} : {},
|
|
3082
4353
|
audit: new FileAuditLog(),
|
|
3083
4354
|
log
|
|
3084
4355
|
});
|
|
@@ -3136,6 +4407,24 @@ function registerPipelineCommands(program) {
|
|
|
3136
4407
|
}
|
|
3137
4408
|
await runPipelineAction(def.id, objective ?? previous.objective);
|
|
3138
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
|
+
});
|
|
3139
4428
|
pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
|
|
3140
4429
|
const config = loadConfig();
|
|
3141
4430
|
const engine = buildPipelineEngine(config);
|
|
@@ -3145,7 +4434,8 @@ function registerPipelineCommands(program) {
|
|
|
3145
4434
|
return;
|
|
3146
4435
|
}
|
|
3147
4436
|
for (const run2 of runs) {
|
|
3148
|
-
|
|
4437
|
+
const note = isInterrupted(run2) ? " (interrupted \u2014 process gone before finish)" : "";
|
|
4438
|
+
console.log(`${run2.id} ${run2.status.padEnd(10)} ${run2.startedAt ?? "?"}${note}`);
|
|
3149
4439
|
}
|
|
3150
4440
|
});
|
|
3151
4441
|
pipeline.command("status <run-id>").description("Show the recorded state of a pipeline run.").action(async (runId) => {
|
|
@@ -3163,6 +4453,13 @@ function registerPipelineCommands(program) {
|
|
|
3163
4453
|
console.log(` status: ${run2.status}`);
|
|
3164
4454
|
console.log(` started: ${run2.startedAt ?? "?"}`);
|
|
3165
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
|
+
}
|
|
3166
4463
|
if (run2.resultSummary) console.log(` result: ${run2.resultSummary}`);
|
|
3167
4464
|
for (const stepRun of run2.stepRuns) {
|
|
3168
4465
|
console.log(` [${stepRun.stepId}] ${stepRun.status}`);
|
|
@@ -3204,6 +4501,9 @@ async function runPipelineAction(id, objective) {
|
|
|
3204
4501
|
function registerRunAlias(program) {
|
|
3205
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));
|
|
3206
4503
|
}
|
|
4504
|
+
function isInterrupted(run2) {
|
|
4505
|
+
return run2.status === "running" && run2.finishedAt === void 0;
|
|
4506
|
+
}
|
|
3207
4507
|
function findPipeline(config, id) {
|
|
3208
4508
|
const def = config.project.pipelines.find((p) => p.id === id);
|
|
3209
4509
|
if (!def) {
|